PHP 程式設計/SSH 類
| 一位讀者要求改進本書的格式和佈局。 良好的格式使書籍更易於閱讀,併為讀者帶來更多興趣。有關想法,請參見編輯維基文字,有關優秀書籍的示例,請參見WB:FB。 即使在刪除此訊息後,也請繼續編輯本書並改進格式。有關當前進展,請參見討論頁面。 |
PHP 有一個類,允許您透過 SSH 連線到伺服器。本教程解釋瞭如何使用它。
致華夏公益教科書管理員:是的,我知道它很亂,我很快就會讓它看起來漂亮:) 我只是認為華夏公益教科書是放置此教程的最合適地方,因為 php.net 評論區認為它有點太長了 :P
致讀者:正如您可能看到的那樣……這是一個非常非常長的教程。不是因為這是一個困難的概念。而是因為我想詳細地解釋它,以便任何人都能理解。如果您已經有一些 PHP 經驗,那也沒關係。瀏覽一下我在下面顯示的原始碼,如果您不理解某一行,只需在教程中查詢它即可。我在 3 個非常重要的概念上用了一堆 *********。
希望這對很多人有所幫助。這是我第一次做如此廣泛的教程。所以給我你的意見:)
我使用 ssh2 時遇到了很多麻煩,主要是因為我不理解許多使用命令背後的概念。這就是為什麼當我嘗試不同使用者在下面給出的指令碼時,它們不起作用。我對使用者使用的每個函式都進行了一些研究……我建立了一個(幾乎)萬無一失的指令碼。最重要的是,我將解釋程式碼中究竟發生了什麼,與這裡許多使用者不同,解釋每個步驟的作用。
這是程式碼。您將使用的函式是 readwrite。它使用使用者名稱/密碼認證透過 ssh 連線,傳送命令並查詢特定輸出。如果找到該輸出,它將返回 true。顯然,您希望做一些不同的事情(例如:獲取命令的所有輸出,執行多個命令等)。這就是為什麼我將解釋每行中究竟發生了什麼。相信我,如果您不理解指令碼的一部分,請閱讀解釋!,你不想在這裡走捷徑,否則如果你的情況與我的情況略有不同,你將得到非常意外的結果。
function readwrite ($write, $lookfor,$ip,$user,$pass) {
flush(); //Write Whatever we have before
//Connect and Authenticate
$connection = ssh2_connect($ip) or die ("can't connect");
ssh2_auth_password($connection,$user,$pass) or die("can't auth");
//Start the Shell
$shell = ssh2_shell($connection,"xterm");
//Here we are waiting for Shell to initialize
usleep(200000); //increase this a bit if you get unexpected results
$write = "echo '[start]'; $write ; echo '[end]'";
$out = user_exec($shell, $write);
fclose($shell);
if(stristr($out, $lookfor)) { //it exists
return true;
}
}
function user_exec($shell,$cmd) {
fwrite($shell,$cmd . PHP_EOL); //write to the shell
$output = ""; //will store our output
$start = false; //have we started yet
$start_time = time(); //the time sarted
$max_time = 10; //time in seconds
while(((time()-$start_time) < $max_time)) { //if the x seconds haven't passed
$line = fgets($shell); //get the next line of output
if(!stristr($line,$cmd)) { //we don't want output that was out command (because it also contains [start] and [end]
if(preg_match('/\[start\]/',$line)) { //if we see that [start] is in the line that means we started
$start = true; //set start to true
}
elseif(preg_match('/\[end\]/',$line)) { //we're done
return $output; //return what we have (last line)
}
elseif($start && isset($line) && $line != "")
{
$output = $line; //return only last line (.= for all lines)
}
}
}
}
function readwrite ($write, $lookfor,$ip,$user,$pass) {
這是我的 readwrite 函式的函式頭。它接受 5 個引數
這是我們要傳送的命令(例如:cat logfile)。它可以是任何有效的 bash 命令。
注意:如果您還不知道,您必須轉義 php 看作“特殊”的所有字元,例如 $ 和引號。例如,如果我想執行 $?(打印出退出狀態),我將不得不轉義 $ 到 \。如果您想傳送多個命令,可以使用“;”分隔它們(例如:command1;command2;command3...)。另一個需要注意的是,您可能想在終端中測試您的命令,然後再在指令碼中使用它們。請記住,您在終端中看到的是您在指令碼中得到的
在我的情況下,我正在尋找 shell 返回某個值(例如:我之前命令的退出狀態)。所以在我的情況下,如果我正在尋找成功的執行,它將是“0”。
這些相當不言自明。基本上,它是我們想要連線到的 IP(或主機名或域名,或者您如何訪問遠端計算機),以及使用者名稱和密碼。它不是您當前計算機的 un/pw。它是遠端計算機的使用者名稱和密碼。請注意,SSH2 類還支援公鑰認證。我還沒有嘗試過,但如果有人想讓我嘗試製作教程,請給我發電子郵件。
這是我們的標題。請記住……這是我的函式。它執行我想要它執行的特定任務(即檢視輸出的最後一行,如果它與 $lookfor 匹配,則返回 true),當給出命令 $write 時。如果您有與我的不同的目的,那麼您必須相應地編輯此指令碼。如果您需要幫助,請給我發電子郵件,我將很樂意提供幫助!
flush(); //Write Whatever we have before
這是可選的。在我的例子中,我在迴圈中執行 readwrite,我想看到結果出現。flush() 的作用是將 echo 緩衝區中的任何內容輸出給使用者(而不是先載入整個指令碼,然後吐出 echo 緩衝區)。如果我錯了,請糾正我。
//Connect and Authenticate
$connection = ssh2_connect($ip) or die ("can't connect");
基本上,$connection 是一個資源。如果您使用過 php 中的 mysql,您會發現這更容易理解。資源是一個與外部程式互動的物件(例如:在您執行 mysql_connect() 後,您就可以訪問 mysql 資源)。它允許程式碼中的其他函式與該資源進行互動。如果它無法連線,它將退出指令碼並給出錯誤(無法連線)
ssh2_auth_password($connection,$user,$pass) or die("can't auth");
ssh2_auth_password 是一個可以與 ssh 資源互動的函式。在本例中,它使用給定的使用者名稱和密碼對您進行身份驗證。如果無法進行身份驗證,它將退出指令碼並顯示錯誤資訊(無法進行身份驗證)。
Shell 流
[edit | edit source] //Start the Shell
$shell = ssh2_shell($connection,"xterm");
$shell 持有一個名為 STREAM 的東西。流是非常酷的東西。您可以將它們與 mysql_query() 進行比較,mysql_query() 也是一種流(我認為)。
深入
[edit | edit source]對於所有流,您都可以讀取它們。它們動態輸出資料,並且有 PHP 函式可以讀取它們輸出的每一行。對於檔案,以及顯然對於 shell 流也是如此,有一個名為 fgets 的函式。它所做的一切就是獲取下一行文字。
如果您熟悉 mysql,您可以想到非常常見的 while 迴圈
while ($row = mysql_fetch_array($query))
fgets 與 mysql_fetch_array 相同。兩者都意味著獲取下一條記錄(或行)。不同的是 mysql_fetch_array 將其作為一組列獲取,而 fgets 將其作為字串獲取。您可能想知道……為什麼我不能只使用 while($line = fgets($shell)) 呢?嗯……從技術上講,您可以這樣做。對於某些目的,它將非常有效。但我會在下面的 fgets 函式中解釋為什麼在大多數情況下這不是一個好主意……
等待 Shell 初始化
[edit | edit source] //Here we are waiting for Shell to initialize
usleep(200000); //increase this a bit if you get unexpected results
一些指令碼沒有提到這一點。嘗試在終端中輸入 ssh servername,其中 servername 是您要 ssh 到伺服器的名稱(或 IP 或域名)。使用您的密碼登入。現在您將看到類似的東西
Linux adz-laptop 2.6.28-14-generic #46-Ubuntu SMP Wed Jul 8 07:21:34 UTC 2009 i686
The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.
To access official Ubuntu documentation, please visit:
http://help.ubuntu.com/
0 packages can be updated.
0 updates are security updates.
Last login: Sun Jul 19 00:06:10 2009 from localhost
透過網路傳輸需要非常短的時間。但是,在某些情況下,它可能需要更長的時間(例如:您使用的是撥號連線)……為了避免此問題,您必須指定一個休眠間隔
usleep(x) 的作用是告訴 PHP 等待 x 微秒(百萬分之一秒)再繼續。將其設定為 200,000(五分之一秒)是比較安全的。如果您開始得到意外的結果,您可能需要將其調高一點。
格式化命令
[edit | edit source] $write = "echo '[start]'; {$write}; echo '[end]'";
還記得我們函式頭部的 $write 嗎?那是我們的命令。[start] 部分對於我的目的來說不是絕對必要的,但對您來說可能是必要的。所以我將解釋正在發生的事情。
fread() 在下面的 user_exec 函式中使用,獲取 shell 中輸出的所有內容。包括我在前面示例中告訴您的標頭。我們不想要這些。我們只想看到我們的命令輸出的內容。這就是為什麼我們首先執行 echo '[start]',它將文字 [start] 寫入螢幕上。然後它執行我們的命令(命令的輸出將顯示在螢幕上)。然後它列印 [end]。所以最後我們應該得到
[start] 命令的輸出 [end]
我們的 user_exec 函式將處理剩下的事情。我會解釋它在裡面發生的時候是如何工作的……
傳送請求並獲取輸出
[edit | edit source] $out = user_exec($shell, $write);
看起來我們已經準備好了傳送請求!我們使用一個名為 user_exec 的小函式,這個函式是由下面的使用者之一編寫的。我稍微修改了一下它以適應我的需要,我會解釋您如何做同樣的事情以適應您的需要。PHP 所做的是執行該函式,並將返回值儲存到 $out 中。$out 將包含您的輸出。在我的情況下,我只想要最後一行。這就是指令碼未編輯版本中的 $out 所包含的內容
關閉 Shell
[edit | edit source] fclose($shell);
當我們完成所有工作後,最好關閉我們的 shell 流。它類似於 mysql_close()。
檢查 Lookfor 的輸入
[edit | edit source] if(stristr($out, $lookfor)) { //it exists
return true;
}
深入
[edit | edit source]stristr 代表字串中的字串。我在上面生成的 $out 中查詢 $lookfor。請記住,這隻有在 $out 是字串的情況下才能工作!如果您希望它是一個數組……請學習一下 foreach 迴圈……或者如果您真的很笨(只是開玩笑!您並不笨,您只是缺乏經驗。這是可以透過練習改變的)給我發郵件描述您的情況,我會教你。不用擔心,我喜歡教:)
USER_EXEC 函式
[edit | edit source]function user_exec($shell,$cmd) {
好的……還記得我們在上面呼叫 user_exec 函式的時候嗎……這就是正在呼叫的內容。我們傳遞了兩個引數給它,即 $shell 流和命令 ($cmd)
寫入 Shell 流
[edit | edit source] fwrite($shell,$cmd . PHP_EOL); //write to the shell
還記得我說過您可以從流中讀取嗎?猜猜看!您也可以寫入它們!fwrite 就是這麼做的!所以我們正在寫入……到我們的 $shell 流,命令和 PHP_EOL。PHP_EOL 代表行尾(相當於您在鍵盤上按下回車鍵)。我認為您可以使用 \r 和 \n 來代替,但有人建議使用 PHP_EOL 來代替……不會有壞處(有人能解釋一下為什麼嗎?)
一些區域性變數
[edit | edit source] $output = ""; //will store our output
$start = false; //have we started yet
$output 儲存我們的輸出,start 表示我們是否已經到達 [start]……但關於這方面的更多資訊請參見下文。
迴圈計時:為什麼它很重要
[edit | edit source] $start_time = time(); //the time sarted
$max_time = 10; //time in seconds
while(((time()-$start_time) < $max_time)) { //if the x seconds haven't passed
重要:假設您的命令是執行一個 shell 指令碼。它運行了……我不知道 3 秒。這就是為什麼 while($line = fgets($shell)) 不能按預期工作的原因。
假設我們使用 while($line = fgets($shell)...,將會發生以下情況:1) 我們建立了 SSH 連線。耶!... 2) 我們獲得了 shell 流。耶!3) 我們等待了幾毫秒,等待奇怪的頭部載入。 4) 我們向 shell 寫入我們的命令,告訴它執行 shell 指令碼。我們的大型 shell 指令碼正在執行。請記住,它需要 2 秒才能執行。對 php 來說,這是一個很長的時間。 4) 它開始迴圈讀取。每次 fgets($shell) 執行時,它都會輸出下一行。所以我們在最多半秒鐘內就完成了頭部處理。我們開始的剝離工作已經完成了四分之一。然後我們讀取下一行。哦!它不存在 O.o。指令碼還沒有輸出任何東西!好吧,PHP 認為...這個傢伙告訴我,只要它返回東西就一直執行 fgets。看來我們在這裡完成了。它退出我們的 while 迴圈 5) shell 指令碼仍在執行另外 1 秒 3/4。它大聲喊出輸出!但是 php 聽不到。所以它就像從未給出過那個輸出一樣。還記得那句話嗎?“如果樹在森林裡倒下,沒有人聽到它...它真的發出聲音了嗎?” 顯然,它沒有 :) (插入我嘲笑自己的俗套笑話!!!) 希望這個例子 + 俗套笑話有助於解釋為什麼 while() 迴圈在執行大型指令碼的情況下不會起作用,所以你可能會想知道...那麼...我們如何獲得該死的 shell 指令碼的輸出? 以下是方法...
$start_time = time(); //the time started
$max_time = 10; //time in seconds
while(((time()-$start_time) < $max_time)) { //if the x seconds haven't passed
它非常聰明,但也非常簡單。下面的使用者建議了它。以下是如何工作的。time() 獲取 unix 時間戳(自 UNIX 紀元開始以來的秒數)。嗯?什麼?別擔心。我們只關心它是以秒為單位的時間。如果你想知道更多關於時間的資訊,只需查詢 time() 函式。它很...有用... $max_time 儲存迴圈可以執行的最大時間。
while(((time()-$start_time) < $max_time)) { //if the x seconds haven't passed
這裡是神奇的地方。每次 php 進行 while 迴圈的迭代時,它都會檢查某個條件。條件是最大時間尚未過去。以下是如何知道。還記得 time() 嗎?它輸出自 UNIX 紀元開始以來的當前時間(以秒為單位)。嗯...不出所料,如果你一分鐘前呼叫了 time(),現在又呼叫了它,time() 的當前結果將更大...大 60 秒。你在小學學過的基本數學。time() 從某個日期開始計算秒數(準確地說,是 1970 年 1 月 1 日)。它會產生一個巨大的數字。但這並不重要,我們只關心起始數字比現在數字小多少秒。這就是 time()-$start_time 的作用!直到該數字達到最大秒數,我們將繼續執行迴圈...一遍又一遍...它將進行相當多的迭代...但這並不重要。
現在是超級重要的部分。你必須確保你要執行的指令碼在 $max_time 中指定的時間內執行完成。嘗試自己執行指令碼,並根據它花費的時間額外新增一兩秒。你可能想知道,為什麼不將 $max_time 設定為一個非常非常大的數字,比如... 1000000000000000000。這樣我的指令碼將有足夠的時間執行。這裡的問題是...如果你的 shell 指令碼中出現錯誤,它由於某種原因沒有退出,或者沒有產生輸出,php 也不知道...它會認為它仍在執行。它將進入一個近乎無限的迴圈,直到你的 100000000000000000 秒過去。這不好。一點也不好....所以要設定一個*合理的*時間。
$line = fgets($shell); //get the next line of output
我們在輸出中獲取下一行,並將其放入 line 中。所以前幾次它將是頭部和我們剛剛給出的命令。如果我們的大型指令碼仍在執行,它會繼續嘗試獲取下一行...並且失敗。一遍又一遍。但我們不在乎。最終,指令碼將產生一些輸出...
if(!stristr($line,$cmd)) { //we don't want output that was out command (because it also contains [start] and [end]
好吧,我們只關心命令的輸出,而不是終端中發生的所有其他隨機內容。這就是我們包含 echo [start] 的原因,它會將一個字面上的 [start] 列印到螢幕上,告訴 php(在下面)開始檢視輸出。但是等等!!!我們用來執行此操作的命令是 echo '[start]'.......它包含 [start] :P!!!! stristr 正如我之前解釋的那樣,在第一個引數中查詢第二個引數。所以如果它找到了我們的命令...我們不想要它。否則我們將繼續(注意 ! 表示 NOT)。有用的運算子
if(preg_match('/\[start\]/',$line)) { //if we see that [start] is in the line that means we started
preg_match 在引數 b 中匹配引數 a.. 有點像 stristr,但方向相反,它使用正則表示式(你可以在 http://www.regular-expressions.info/ 上閱讀它們。它們非常有用,但是...對於這個面向 n00b 的教程來說太高階了)。如果你想了解更多資訊,請給我發電子郵件(嗯...剛想到一個主意...也許我應該建立一個網站,向 n00bs 解釋高階概念....)
$start = true; //set start to true
}
基本上,如果我們匹配 [start],我們不希望它出現在我們的輸出中,但我們希望告訴 php 這之後的任何內容都是輸出。因此,我們將布林值 $start 設定為 true。
elseif(preg_match('/\[end\]/',$line)) { //we're done
return $output; //return what we have (last line)
}
如果它匹配 [end],則意味著是停止的時候了。因此,它返回我們的輸出,儲存在 ($output) 中,這會中斷迴圈。它儲存在我們傳送 user_exec() 結果的任何變數中。
elseif($start && isset($line) && $line != "")
{
好吧,如果它不是開頭或結尾,那麼它可能是兩種情況之一:我們想要的輸出,或者我們不想要的輸出。這就是 $start 的作用。回想一下我們的第一個 if 語句。如果它看到了 [start],它會將 $start 設定為 true。否則它為 false。如果你記得布林值和條件是如何工作的,&& 運算子表示確保 ___ 和 ___ 都為 true。所以這裡 $start 和 isset($line) 以及 $line != "" 都必須返回 TRUE。如果 $start 由我們的頂部 if 塊設定為 true,耶,我們繼續。isset() 是一個函式,如果某個變數...已設定,則返回 true。現在,記住我們可憐的 $line 變數。在 shell 指令碼執行的所有那些時間裡,它沒有被設定為任何東西,因為 fgets($shell) 沒有返回任何東西!!!所以基本上 isset($line) 將檢查這一點。如果沒有,那麼...是時候重新啟動迴圈了。一遍又一遍,直到 $line 中最終出現了一些東西。而 $line != "" 如果 $line 不是空行,則返回 true。我們不想要空行...或者我們想要...由你決定。我不需要用於我的目的。但現在你知道如果需要該怎麼做。
$output = $line; //return only last line (.= for all lines)
所以假設我們的所有三個條件都匹配。在這一行之前有一個 [start],$line 被設定為了一些東西,並且它不是空白的。
重要:這裡發生的事情是,每次它執行時,它都會將 $output 重置為最新行的內容。這就是我想要的。我只想要最後一行。它會不斷替換 $output,一行一行,直到我們在 [end] 中讀取,在這種情況下,$output 只會儲存最後一行。如果你想要所有輸出,你可以執行以下兩種操作之一
1) 執行 $output .= $line . "
"; <- 這是在將輸出輸出到螢幕上的情況下。你將 line 的內容和一個換行符追加 (.=) 到 $output。 2) 執行 $output[] = $line; <- 這會將該行新增到陣列中。這樣你就可以透過 php 對它進行進一步分析。
就是這樣!現在你應該瞭解此指令碼的每個部分是如何工作的、如何編輯它以及更多內容。即使你是 PHP 的初學者。難道不簡單嗎?如果你仍然不明白某些東西,可以給我傳送電子郵件到 adz@jewc.org。這是我第一次做如此超級超級詳細的教程,所以我想要你的反饋!!!