2011年8月29日月曜日

用php遠程關機和觀察遠程桌面

一、因由

  民國100年上半年,我用了3年半的電腦突然罷工,啟動不起來了,打開電源後屏幕一直是黑的,就是亮不起來。吃不准到底是主板壞了,還是電源,又或者是顯卡壞了。於是,買了最便宜的帶集成顯卡的主板:ASRock的G41M-VS2,又買了個電源。經試驗是主板犧牲了,而顯卡也變得十分之不穩定,不時地花屏,且摸上去很燙。不得已只能拋棄了老主板和顯卡,但是就G41M-VS2主板及其顯卡的性能又遠遠滿足不了我的要求,於是又買了更新型的ASUS的P8H67-V主板。可是老的CPU和內存並不能用在新的主板上,只好又買了新的CPU和內存。正好原來的硬盤容量也不太夠了,於是順便還買了個硬盤。就這樣,我手上多出了1個帶顯卡的主板,1個電源,1個CPU,2根內存,1塊硬盤。這麼多閒置的硬件,本着不浪費的原則,就買了個最便宜的主機箱,拼出了一台不帶顯示器的電腦。因為沒有多餘的顯示器,這台電腦我就打算用它來做下載電影電視劇什麼的服務器。本來就只是廢物利用,也就不想花錢給它買操作系統了,給它裝個免費的Linux吧。因為以前用過Debian,所以先試著安裝了Debian。安裝很順利地完成了,可是啟動時卻直接就死機了,進不了桌面。查看了啟動日誌,也沒看出什麼有用的東西,只知道可能是Debian不支持G41M-VS2主板的集成顯卡。上網查了半天,也沒弄明白個所以然,倒是偶然發現Ubuntu是支持這個主板的。Ubuntu本來就是在Debian的基礎上開發的,於是也不研究為什麼Debian不行了,轉而安裝Ubuntu。Ubuntu果然是個好東西啊,十分順利地安裝完,啟動進入了桌面。因為將來沒有顯示器給它用,所以開啟了自動登陸桌面和遠成桌面服務。然後在常用的電腦上裝上vnc客戶端,這樣就可以在遠程操作服務器了。用了幾天,感覺不錯,白天出去上班時也開著它。過了兩天突然想,白天上班時如果下載完了我也不知道,就這麼一直開着豈不是很浪費電,要是能遠程查看服務器桌面和關機就好了。雖然通過vnc可以遠程控制桌面,但是那需要向外開放vnc服務的端口,這樣實在是太不安全了,況且我並不需要完全得遠程控制我的桌面,因此我設想在服務器上搭建一個https的服務,然後通過訪問網頁來關機,或者拷貝屏幕並生成圖片來顯示在網頁上。於是我在服務器上安裝了apache2和php5,並開啟了ssl,安裝配置過程就略過不提了。



二、遠程關機

  實現的想法是用php執行關機的命令。



 1.在php裏執行halt命令

  由於halt命令需要管理者權限,因此要用sudo來執行。

PHP code
exec('sudo /sbin/halt');

 2.配置sudo

  由於sudo命令需要輸入密碼,因此不修改sudo設置的話,用php執行時會報沒有終端輸入密碼的錯誤。

  打開/etc/sudoers文件,為用戶www-data添加halt無需密碼的權限。

配置文件
www-data ALL=NOPASSWD:/sbin/halt

  這樣通過網頁就能直接關機了。



三、遠程查看桌面

  開始的想法是用php執行屏幕拷貝的命令,但是試驗下來的結果是桌面的進程用戶和http訪問的用戶不同,因此屏幕拷貝的命令就會報“x window進程找不到”的錯。後來想起在網上曾經看到過用java applet當vnc客戶端的文章,所以想到用php就算做不了完整的vnc客戶端,取得屏幕應該不成問題吧。(因為完整的vnc客戶端需要實現实時描繪圖像,而通過php描繪圖像後再傳到瀏覽器顯示的方法是怎麼也達不到实時的效果的。)於是花時間研究了vnc所採用的RFB協議,和tvnjviewer的源代碼,終於實現了用php獲取遠程桌面。



 1.RFB協議介紹

  由於我是用RFB 3.7版協議實現的,因此下面介紹RFB 3.7協議的內容。

  1) 首先,客戶端向服務器的5900端口發出連接請求。

PHP code
$fp = fsockopen('localhost', '5900', $errno, $errstr, 30);

  2) 服務器向客戶端發送最高支持的版本號。

   客戶端 <-- 12字節的字符串 --- 服務器 內容:版本號(例:RFB 003.007\n)

PHP code
$r = fread($fp, 12);

  3) 客戶端向服務器發送使用的版本號。

   客戶端 --- 12字節的字符串 --> 服務器 內容:版本號

PHP code
fwrite($fp, "RFB 003.007\n");

  4)服務器向客戶端發送連接的安全類型。

   客戶端 <-- n個字節的數值 --- 服務器 內容:第一個字節為支持的安全類型的個數,後面n-1個字節就是各個安全類型。如果第一個字節是零,則表示連接失敗。安全類型(0:不可用,1:無安全類型,2:VNC認証,其他請參考RFB協議)

PHP code
$r = fread($fp, 1);

//count of security types
$cnt = ord($r);
//get security types
$r = fread($fp, $cnt);

  5) 客戶端向服務器發送使用的安全類型。

   客戶端 --- 1個字節的數值 --> 服務器 內容:使用的安全類型

PHP code
$securitype = 1;

fwrite($fp, $securitype);

   如果使用VNC認証,將多進行步驟①~③

   ① 服務器向客戶端發送16字節的隨機字符串

   客戶端 <-- 16字節的字符串 --- 服務器 內容:隨機字符串

PHP code
$r = fread($fp, 16);

   ② 客戶端用用戶輸入的密碼作為密钥,DES加密字符串後發送給服務器

   客戶端 --- 16字節的字符串 --> 服務器 內容:加密後的字符串

PHP code
//密碼使用前必須按二進制位調換

function mirrorBits($k) {
$arr = unpack('c*', $k);
$ret = '';
$cnt = count($arr);
if($cnt > 8){
$cnt = 8;
}

for($i=1; $i<=$cnt; $i++){
$s = $arr[$i];
$s = (($s >> 1) & 0x55) | (($s << 1) & 0xaa);
$s = (($s >> 2) & 0x33) | (($s << 2) & 0xcc);
$s = (($s >> 4) & 0x0f) | (($s << 4) & 0xf0);
$ret = $ret . chr($s);
}

return $ret;
}
//加密字符串
$des = mcrypt_encrypt(MCRYPT_DES, mirrorBits($pwd), $r, MCRYPT_MODE_ECB);
//發送加密後字符串 (16字節)
fwrite($fp, $des);

   ③ 服務器向客戶端發送驗證結果。

   客戶端 <-- 4字節的數值 --- 服務器 內容:32位無符號整數(0:OK,1:失敗)

PHP code
//get security check result

$r = fread($fp, 4);
$data = unpack('N', $r);
if($data[1] != 0){
setError('Authentication is failed.');
return false;
}

  6) 客戶端向服務器發送共享標誌。

   客戶端 --- 1個字節的數值 --> 服務器 內容:共享標誌(0:不共享,非0:共享)

PHP code
fwrite($fp, chr(1));

  7) 服務器向客戶端發送初始消息。

   客戶端 <-- n個字節的數據 --- 服務器 內容:2字節+2字節+16字節+4字節+剩餘字節

     2字節:16位無符號整數(幀緩存寬度)

     2字節:16位無符號整數(幀緩存高度)

    16字節:像素格式(1字節+1字節+1字節+1字節+2字節+2字節+2字節+1字節+1字節+1字節+3字節)

        1字節:8位無符號整數(一個顏色的位數)

        1字節:8位無符號整數(深度)

        1字節:8位無符號整數(big-endian標誌)

        1字節:8位無符號整數(真彩標誌)

        2字節:16位無符號整數(紅色最大值)

        2字節:16位無符號整數(綠色最大值)

        2字節:16位無符號整數(藍色最大值)

        1字節:8位無符號整數(紅色替換)

        1字節:8位無符號整數(綠色替換)

        1字節:8位無符號整數(藍色替換)

        3字節:填充

     4字節:32位無符號整數(名字長度)

   剩餘字節:名字字符串

PHP code
$r = fread($fp, 24);

$data = unpack('n2size/C4flag/n3max/C3shift/x3skip/Nslen', $r);
$width = $data['size1'];
$height = $data['size2'];
$bitsPerPixel = $data['flag1'];
$depth = $data['flag2'];
$bigEndianFlag = $data['flag3'];
$trueColorFlag = $data['flag4'];
$redMax = $data['max1'];
$greenMax = $data['max2'];
$blueMax = $data['max3'];
$redShift = $data['shift1'];
$greenShift = $data['shift2'];
$blueShift = $data['shift3'];
$slen = $data['slen'];
//server name
$r = fread($fp, $slen);
$serverName = $r;

  8) 客戶端向服務器發送使用的編碼方式。

   客戶端 --- n個字節的數據 --> 服務器 內容:設置編碼方式(1字節+1字節+2字節+k×4字節)

     1字節:8位無符號整數(2:設置編碼方式)

     1字節:填充

     2字節:16位無符號整數(編碼方式個數),即k

     4字節:32位無符號整數(編碼方式)

     編碼方式:0 Raw、1 CopyRect、2 RRE、5 Hextile、16 ZRLE等(建議用ZRLE)

PHP code
//向服務器發送使用編碼ZRLE方式和Raw方式

$req = pack('C2n1N2', 2, 0, 2, 16, 0);
fwrite($fp, $req);

  9) 客戶端向服務器發送幀緩存更新請求。

   客戶端 --- 10個字節的數據 --> 服務器 內容:幀緩存更新請求(1字節+1字節+2字節+2字節+2字節+2字節)

     1字節:8位無符號整數(3:幀緩存更新請求)

     1字節:8位無符號整數(增量標誌)

     2字節:16位無符號整數(x座標)

     2字節:16位無符號整數(y座標)

     2字節:16位無符號整數(寬度)

     2字節:16位無符號整數(高度)

PHP code
$req = pack('C2n4', 3, 0, 0, 0, $width, $height);

fwrite($fp, $req);

  10) 服務器向客戶端發送幀緩存更新內容。

   客戶端 <-- n個字節的數據 --- 服務器 內容:幀緩存更新(1字節+1字節+2字節+k×(2字節+2字節+2字節+2字節+4字節+m字節))

     1字節:8位無符號整數(0:幀緩存更新)

     1字節:填充

     2字節:16位無符號整數(矩形個數)

    矩形內容

     2字節:16位無符號整數(x座標)

     2字節:16位無符號整數(y座標)

     2字節:16位無符號整數(寬度)

     2字節:16位無符號整數(高度)

     4字節:32位無符號整數(編碼類型)

     m字節:特定編碼的數據

PHP code
$r = fread($fp, 4);

$data = unpack('Cflag/x/ncount', $r);
for($i=0; $i<$data['count']; $i++){
//獲得矩形信息
$r = fread($fp, 12);
$rect = unpack('nx/ny/nwidth/nheight/Ntype', $r);
//TODO: 描繪矩形
......
}

 2.解碼

  到這裡,我們向服務器請求了整個屏幕的幀緩存,然後再把服務器回給我們的幀緩存數據解碼出來,繪製成圖片的話,我們就達到了拷貝屏幕的目的。所以下面就說說特定的編碼如何把它解碼出來。根據RFB協議,Raw編碼是一定要實現的,而ZRLE編碼的效率很高,因此,我就實現了這兩種編碼。

  1) Raw編碼

   Raw編碼的矩形數據流是單純的從左到右,從上到下的像素顏色數據,因此字節數為寬×高×顏色的字節數。

PHP code
//1行的字節數

$readmax = $rect['width']*$bitsPerPixel/8;
for($i=0; $i<$rect['height']; $i++){
$r = fread($fp, $readmax);
$rarr = unpack('C*', $r);
if(count($rarr) < $readmax){
$errmsg = 'Raw data is not correct.';
return false;
}
for($j=0; $j<$rect['width']; $j++){
$offset = $j*4+1;
$red = $rarr[$offset+$redShift/8];
$green = $rarr[$offset+$greenShift/8];
$blue = $rarr[$offset+$blueShift/8];
$color = imagecolorallocate($img, $red, $green, $blue);
if(imagesetpixel($img, $rect['x']+$j, $rect['y']+$i, $color) == false){
$errmsg = 'Draw color failed.';
return false;
}
}
}

  2) ZRLE編碼

   ZRLE編碼的矩形數據流是由4字節+n字節,头4字節是deflater壓縮後的數據字節數,後面跟着的就是壓縮數據。壓縮數據可以用gzinflate解壓(去掉頭2個字節後可解壓),解壓後是64×64像素CPIXEL的數據。(CPIXEL是3個字節)數據的第一個字節是子編碼,如果片被運行長度編碼,那麼本字節的最高位就會被設置。剩下7位表示調色板,0表示沒有調色板,1表示單色,2到127表示調色板的顏色數。

   ① 解壓ZRLE編碼的矩形數據流,然後以64×64像素的小矩形來循環。

PHP code
if(!extension_loaded('zlib') || !strstr($_SERVER['HTTP_ACCEPT_ENCODING'], 'deflate')){

$errmsg = 'php zlib has not been loaded.';
return false;
}
$r = fread($fp, 4);
$ziplen = unpack('N', $r);
$zipr = fread($fp, $ziplen[1]);
//must strip the first 2 bytes off it to inflate.
$r = gzinflate(substr($zipr, 2));

$tileSize = 64;
$data = unpack('C*', $r);
$maxX = $rect['x'] + $rect['width'];
$maxY = $rect['y'] + $rect['height'];
$idx = 1;
for($tileY = $rect['y']; $tileY < $maxY; $tileY += $tileSize){
$tileHeight = min($maxY - $tileY, $tileSize);
for ($tileX = $rect['x']; $tileX < $maxX; $tileX += $tileSize){
$tileWidth = min($maxX - $tileX, $tileSize);

//子編碼
$subtype = $data[$idx];
$idx++;

//TODO: 描繪64×64小矩形
......
}
}

   ② 子編碼 0:Raw,子編碼後面是寬×高×bytesPerCPixel字節的CPIXEL顏色數據。

PHP code
for($i=0; $i<$tileHeight; $i++){

for($j=0; $j<$tileWidth; $j++){
$offset = $idx;
$red = $data[$offset+$redShift/8];
$green = $data[$offset+$greenShift/8];
$blue = $data[$offset+$blueShift/8];
$color = imagecolorallocate($img, $red, $green, $blue);
$idx = $idx+3;
if(imagesetpixel($img, $tileX+$j, $tileY+$i, $color) == false){
$errmsg = 'Draw color failed.';
return false;
}
}
}

   ③ 子編碼 1:單色,子編碼後面是bytesPerCPixel字節的CPIXEL顏色數據。

PHP code
$offset = $idx;

$red = $data[$offset+$redShift/8];
$green = $data[$offset+$greenShift/8];
$blue = $data[$offset+$blueShift/8];
$color = imagecolorallocate($img, $red, $green, $blue);
$idx = $idx+3;
if(imagefilledrectangle($img, $tileX , $tileY, $tileX+$tileWidth-1, $tileY+$tileHeight-1, $color) == false){
$errmsg = 'Fill color failed.';
return false;
}

   ④ 子編碼 2~16:帶調色板,子編碼的值就是調色板的顏色數。

    子編碼後面是顏色數×bytesPerCPixel字節的CPIXEL顏色數據,再後面是打包像素數據。打包像素數據的內容是矩形從左到右,從上到下,像素顏色在調色板的索引值。顏色數是2時,一個像素占1位,顏色數是3~4時,一個像素占2位,顏色數是5~16時,一個像素占4位。如果寬不是8,4,2的倍數時,為了使各行字節數正確,填充位將被使用。因此,顏色數是2時,打包像素數據的字節數就是floor((寬+7)/8)×高。顏色數是3~4時,字節數就是floor((寬+3)/4)×高。顏色數是5~16時,字節數就是floor((寬+1)/2)×高。

PHP code
//get palette color

$colarr = array();
for($i=0; $i<$subtype; $i++){
$offset = $idx;
$red = $data[$offset+$redShift/8];
$green = $data[$offset+$greenShift/8];
$blue = $data[$offset+$blueShift/8];
$color = imagecolorallocate($img, $red, $green, $blue);
$idx = $idx+3;
$colarr[$i] = $color;
}
//caculate bits per pixel
$bitsPerPixel = 1;
if($subtype > 4){
$bitsPerPixel = 4;
}else if($subtype > 2){
$bitsPerPixel = 2;
}
$pcnt = 8/$bitsPerPixel;
$bcnt = floor(($tileWidth+$pcnt-1)/$pcnt);
//draw rect
for($i=0; $i<$tileHeight; $i++){
for($j=0; $j<$bcnt; $j++){
for($k=0; $k<$pcnt; $k++){
//color index
$cidx = ((1 << $bitsPerPixel) - 1) & ($data[$idx] >> (($pcnt-1-$k)*$bitsPerPixel));
$w = $j*$pcnt+$k;
if($w < $tileWidth){
if(imagesetpixel($img, $tileX+$w, $tileY+$i, $colarr[$cidx]) == false){
$errmsg = 'Draw color failed.';
return false;
}
}else{
break;
}
}
$idx++;
}
}

   ⑤ 子編碼 17~127:未使用。



   ⑥ 子編碼 128:簡單RLE(run-length encoding,行程長度編碼)。子編碼後面是“一個顏色+長度”的結構重複到最後。

    例:紅3黃8藍268紫41……

    一個顏色:bytesPerCPixel字節的CPIXEL顏色數據

    長度:m字節的數值(此數值就是長度值-1,以非255的字節為最後一個字節)。

PHP code
$i = 0;//y pos

$j = 0;//x pos
while($i < $tileHeight){
$offset = $idx;
$red = $data[$offset+$redShift/8];
$green = $data[$offset+$greenShift/8];
$blue = $data[$offset+$blueShift/8];
$color = imagecolorallocate($img, $red, $green, $blue);
$idx = $idx+3;
//color's repeat length-1
$pcnt = 0;
//caculte length (length-1 equals sum all bytes ending with none 255)
do{
$pcnt = $pcnt + $data[$idx];
$idx++;
}while($data[$idx-1] == 255);
//draw the color
for($k=0; $k<=$pcnt; $k++){
if(imagesetpixel($img, $tileX+$j, $tileY+$i, $color) == false){
$errmsg = 'Draw color failed.';
return false;
}
if($j >= $tileWidth-1){
$j = 0;
$i++;
}else{
$j++;
}
}
}

   ⑦ 子編碼 129:未使用。



   ⑧ 子編碼 130~255:帶調色板的RLE。調色板的顏色數=子編碼的值-128。

    子編碼後面是顏色數×bytesPerCPixel字節的CPIXEL顏色數據,再後面是“一個顏色索引+長度”
的結構重複到最後。

    長度是1的時候,結構為一個字節的顏色索引。

    長度大於1的時候,第一個字節為顏色索引+128,後面是和簡單RLE同樣的長度值。

PHP code
//get palette color

$colarr = array();
for($i=0; $i<($subtype-128); $i++){
$offset = $idx;
$red = $data[$offset+$redShift/8];
$green = $data[$offset+$greenShift/8];
$blue = $data[$offset+$blueShift/8];
$color = imagecolorallocate($img, $red, $green, $blue);
$idx = $idx+3;
$colarr[$i] = $color;
}

//draw image
$i = 0;//y pos
$j = 0;//x pos
while($i < $tileHeight){
//color index
$cidx = $data[$idx];
$idx++;
//color's repeat length-1
$pcnt = 0;
//If the top bit is set, then the length is more than one.
if(($cidx >> (8-1)) == 1){
//caculte length (length-1 equals sum all bytes ending with none 255)
do{
$pcnt = $pcnt + $data[$idx];
$idx++;
}while($data[$idx-1] == 255);

$cidx = $cidx-128;
}
//draw the color
for($k=0; $k<=$pcnt; $k++){
if(imagesetpixel($img, $tileX+$j, $tileY+$i, $colarr[$cidx]) == false){
$errmsg = 'Draw color failed.';
return false;
}
if($j >= $tileWidth-1){
$j = 0;
$i++;
}else{
$j++;
}
}
}

  最後將繪製好的圖像保存成jpeg格式,任務就完成了。

2011年7月27日水曜日

Create and install self-signed SMIME certificate for thunderbird

  I use microsoft's makecert.exe to create my certificate. You can get this tool from Microsoft Windows SDK.

  If you have only one email that needs encrypt and sign, do the steps below.
  I. Create a certificate authority with exchange type. Sample:
makecert -r -pe -n "CN=zbtest" -a md5 -sky exchange -cy authority -sp "Microsoft Strong Cryptographic Provider" -b 01/01/2011 -e 01/01/2100 -len 1024 -sv zbtest.pvk zbtest.cer

  II. Create a pfx file for the certificate. Sample:
cert2spc zbtest.cer zbtest.spc
pvk2pfx -pvk zbtest.pvk -spc zbtest.spc -pfx zbtest.pfx

  III. Import certificate authority to thunderbird.
  1. In Thunderbird, go to "Tools -> Options... -> Advanced -> Certificates -> Manage Certificates...".

  2. Go to the "Authorities" tab.

  3. Click on "Import".

  4. Select the "zbtest.cer" file.

  5. It will ask you for what purposes you want to trust the certificate. Select "Trust this CA to identify email users."

  6. Click "OK" to complete the import.

  IV. Import personal certificate to thunderbird.
  1. In Thunderbird, go to "Tools -> Options... -> Advanced -> Certificates -> Manage Certificates...".

  2. Go to the "Your Certificates" tab.

  3. Click on "Import".

  4. Select the "zbtest.pfx" file.

  5. It will ask you for the master password for the software security device. Enter your master password and click "OK".

  6. Next, it will ask you for the password protecting your personal certificate. If your pfx file has a password, enter it here, otherwise leave this field empty. Then click "OK".

  If you have multiple emails that needs encrypt and sign, do the steps below.
  I. Create a certificate authority. Sample:
makecert -r -pe -n "CN=zbtest_ca" -a md5 -sky signature -cy authority -sp "Microsoft Strong Cryptographic Provider" -b 01/01/2011 -e 01/01/2100 -len 1024 -sv zbtestca.pvk zbtestca.cer

  II. Import certificate authority to thunderbird.
  1. In Thunderbird, go to "Tools -> Options... -> Advanced -> Certificates -> Manage Certificates...".

  2. Go to the "Authorities" tab.

  3. Click on "Import".

  4. Select the "zbtestca.cer" file.

  5. It will ask you for what purposes you want to trust the certificate. Select "Trust this CA to identify email users."

  6. Click "OK" to complete the import.

  III. Create a personal certificate for email. Sample:
makecert -pe -n "CN=zbtest_mail_aaa;E=zbtest@aaa.com" -a md5 -sky exchange -ic zbtestca.cer -iv zbtestca.pvk -sp "Microsoft Strong Cryptographic Provider" -b 01/01/2011 -e 01/01/2100 -len 1024 -sv zbtestmail_aaa.pvk zbtestmail_aaa.cer

  IV. Create a pfx file for personal certificate. Sample:
cert2spc zbtestmail_aaa.cer zbtestmail_aaa.spc
pvk2pfx -pvk zbtestmail_aaa.pvk -spc zbtestmail_aaa.spc -pfx zbtestmail_aaa.pfx

  V. Import personal certificate to thunderbird.
  1. In Thunderbird, go to "Tools -> Options... -> Advanced -> Certificates -> Manage Certificates...".

  2. Go to the "Your Certificates" tab.

  3. Click on "Import".

  4. Select the "zbtestmail_aaa.pfx" file.

  5. It will ask you for the master password for the software security device. Enter your master password and click "OK".

  6. Next, it will ask you for the password protecting your personal certificate. If your pfx file has a password, enter it here, otherwise leave this field empty. Then click "OK".

  VI. For another email loop step III to step V.

  Note: The pvk2pfx command will pop up an export wizard if -pfx option is not given.