跳转到内容

PHP 编程/SSH 类

来自维基教科书,开放的书籍,为开放的世界

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)
            }
        }
    }
}

脚本解释

[编辑 | 编辑源代码]

ReadWrite 标头

[编辑 | 编辑源代码]
function readwrite ($write, $lookfor,$ip,$user,$pass) {

这是我的 readwrite 函数的函数头。它接受 5 个参数

这是我们要发送的命令(例如:cat logfile)。它可以是任何有效的 bash 命令。

注意:如果您还不知道,您必须转义 php 看作“特殊”的所有字符,例如 $ 和引号。例如,如果我想运行 $?(打印出退出状态),我将不得不转义 $ 到 \。如果您想发送多个命令,可以使用“;”分隔它们(例如:command1;command2;command3...)。另一个需要注意的是,您可能想在终端中测试您的命令,然后再在脚本中使用它们。请记住,您在终端中看到的是您在脚本中得到的

在我的情况下,我正在寻找 shell 返回某个值(例如:我之前命令的退出状态)。所以在我的情况下,如果我正在寻找成功的运行,它将是“0”。

$ip, $user, $pass
[编辑 | 编辑源代码]

这些相当不言自明。基本上,它是我们想要连接到的 IP(或主机名或域名,或者您如何访问远程计算机),以及用户名和密码。它不是您当前计算机的 un/pw。它是远程计算机的用户名和密码。请注意,SSH2 类还支持公钥认证。我还没有尝试过,但如果有人想让我尝试制作教程,请给我发电子邮件。

这是我们的标题。请记住……这是我的函数。它执行我想要它执行的特定任务(即查看输出的最后一行,如果它与 $lookfor 匹配,则返回 true),当给出命令 $write 时。如果您有与我的不同的目的,那么您必须相应地编辑此脚本。如果您需要帮助,请给我发电子邮件,我将很乐意提供帮助!

Flush() 命令

[编辑 | 编辑源代码]
    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,但方向相反,它使用正则表达式(你可以在 https://regexper.cn/ 上阅读它们。它们非常有用,但是...对于这个面向 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 的初学者。难道不简单吗?如果你仍然不明白某些东西,可以给我发送电子邮件到 [email protected]。这是我第一次做如此超级超级详细的教程,所以我想要你的反馈!!!


华夏公益教科书