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格式,任務就完成了。