PHP Programming/SSH Class
A reader requests that the formatting and layout of this book be improved. Good formatting makes a book easier to read and more interesting for readers. See Editing Wikitext for ideas, and WB:FB for examples of good books. Please continue to edit this book and improve formatting, even after this message has been removed. See the discussion page for current progress. |
PHP has a class that allows you to connect to servers through SSH. This tutorial explains how to use it.
Note to WikiBooks admins: yes, I know it's messy, I'll make it look nice really soon :) I just think that WikiBooks was the most appropriate place to dump the tutorial since the php.net comment section thought it was a bit too long :P
Note to Readers: As you probably can see...this is a really really long tutorial. Not because it's a hard concept. But because I want to explain it in such detail that ANYBODY can get it. If you have a little bit of experience in php already, it's fine too. Look through the source code I showed below and if you don't understand a line, just look for it in my tutorial. I marked off 3 very important concepts with a bunch of *********s.
Hope this helps a buncha people. It's my first time doing such an extensive tutorial. So gimmie your input :)
I've had quite a bit of trouble getting ssh2 to work, mainly because I didn't understand the concepts behind a lot of the commands used. That's why when I tried the scripts given by different users below, the didn't work. I did a bit of research on each of the functions users used...and I've made an (almost) failsafe script. And most importantly I will explain what exactly is happening in the code, unlike a lot of users here, explain what every step does.
Here's the code. The function you will use is readwrite. It connects through ssh using username/password authentication, sends a command and looks for a certain output. If it finds that output it returns true. Obviously you will want to do something different (ex: get ALL the output of a command, run multiple commands, etc). That's why I will explain exactly what is happening in every line. Trust me, if you don't understand a part of the script READ THE EXPLANATION!, you don't want to take any shortcuts here, or you will get very unexpected results if your situation is a slightly different from mine.
Full Script
editfunction 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)
}
}
}
}
Script Explanation
editReadWrite Header
editfunction readwrite ($write, $lookfor,$ip,$user,$pass) {
This is a function header for my readwrite function. It accepts 5 parameters:
In Depth
edit$write
editThis is the command we want to send (ex: cat logfile). It can be any valid bash command.
NOTE: In case you don't know already, you have to escape all characters that php sees as "special", such as $ and quotes. For example if I wanted to run $? (prints out the exit status) I will have to escape $ to \$. If you want to send multiple commands you can separate them with ';' (ex: command1;command2;command3...) Another note is you might want to test out your commands in a terminal before you use them in a script. Remember that what you see in your terminal is what you'll get in the script
$lookfor
editIn my situation I was looking for shell to return a certain value (ex: the exit status of my previous command). So in my situation, if I was looking for a successful run, it would be "0".
$ip, $user, $pass
editThese are pretty self explanatory. Basically it's the IP that we want to connect to (or hostname or domain name or however you access your remote computer), and the username and password. It is NOT your current computer's un/pw. It's the REMOTE computer's username and password. Note that the SSH2 class also supports public key authentication. I haven't tried it but if anybody wants me to try and make a tutorial shoot me an email.
That's is for our header. Remember...this is MY function. It does a specific tasks that I want it to (that is look at the last line of output and return true if it matches $lookfor) when given command $write. If you have a different purpose than mine then you have to edit this script accordingly. If you need help, email me, I will be glad to help!
Flush() Command
edit flush(); //Write Whatever we have before
This is optional. In my case I was running readwrite in a loop and I wanted to see the results as they came up. What flush() does is it outputs whatever we have in our echo buffer to the user (as opposed to loading the entire script first and then spitting out the echo buffer). Correct me if I'm wrong on this.
Connection
edit //Connect and Authenticate
$connection = ssh2_connect($ip) or die ("can't connect");
Basically what $connection is, is a resource. If you have used mysql with php you'll find this a lot easier to understand. A resource is an object which interacts with an external program (ex: after you do a mysql_connect() you have access to the mysql resource). It allows other function within your code to interact with that recourse. If it's can't connect it quits the script and gives an error (can't connect)
Authentication
edit ssh2_auth_password($connection,$user,$pass) or die("can't auth");
ssh2_auth_password is a function that can interact with the ssh resource. In this case it authenticates you with given username and password. It it can't it quits the script and gives off an error. (can't auth)
The Shell Stream
edit //Start the Shell
$shell = ssh2_shell($connection,"xterm");
$shell holds something called a STREAM. Now streams are very cool things. You can compare them to mysql_query() which is a type of a stream (I think).
In Depth
editFor all streams you can read them. They spit out data dynamically and there are php functions to read each line they spit out. For files, and apparently for the shell stream too there's a function called fgets. All that it does is gets the next line of text.
If you are familiar with mysql you can think of the very commonly while loop
while ($row = mysql_fetch_array($query))
fgets is the same as mysql_fetch_array. Both mean get the next record (or line). Except mysql_fetch_array gets it is an array of columns and fgets gets it as a string. You might be wondering...why can't I just do while($line = fgets($shell)). Well.....technically you can. And for some purposes it will work very well. But I'll explain below why it's not a really good idea in *most* cases when we get to the fgets function below...
Wait For Shell To Initialize
edit //Here we are waiting for Shell to initialize
usleep(200000); //increase this a bit if you get unexpected results
A few scripts fail to mention this. Try going in a terminal and typing ssh servername where servername is the name of the server you are trying to ssh into (or ip or domain). Login with your password. Now you'll see something like this:
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
It takes a very short amount of time to transmit over the network. However there are cases where it can take a bit longer (ex: you are on a dialup connection)... In order to avoid this problem, you have to specify a sleep interval
What usleep(x)
does it tells php to wait x microseconds (1 millionth of a second) before going on. It is farely sage to make this 200,000 (1/5 of a second). If you start getting unexpected results you might want to nudge it up a bit.
Formatting The Command
edit $write = "echo '[start]'; {$write}; echo '[end]'";
Remember $write from our function header? That was our command. The [start] part is not absolutely necessary for my purpose, but it might be for yours. So I'll explain what is happening.
fread() which is used in the user_exec function below, gets EVERYTHING that was outputted in the shell. That includes the headers I told you about in our previous example. We don't want that. We only want to see what our command outputs. That's why we first execute echo '[start]' which writes the word [start] literally onto the screen. Then it executes out command (the output from the command goes on the screen). And then it prints [end]. So in the end we should have:
[start] output from command [end]
Our user_exec function will take care of the rest. I will explain how when in happens in there...
Sending the Request and Getting the Output
edit $out = user_exec($shell, $write);
Looks like we are all set to send our request! We use a little function that one of the users below wrote called user_exec. I modified it slightly to fit my needs and I'll explain how you can too to fit yours. What php does is executes that function, and stores the resutn value into $out. $out will contain your output. In my case I just wanted the very last line. That's what $out holds in the unedited version of the script
Closing Shell
edit fclose($shell);
When we're all done it's good practice to close our shell stream. It's comparable to mysql_close().
Checking Out Input for Lookfor
edit if(stristr($out, $lookfor)) { //it exists
return true;
}
In Depth
editstristr stands for string in string. I am looking for $lookfor in the $out which was produced above. Remember that this will only work in the case that $out is a string! If you want it to be an array...read up on foreach loops...or if you're really dumb (jk jk! you're not dumb you're just inexperienced. that can be changed with practice) email me your situation and I'll teach you how. Don't worry I like to teach :)
THE USER_EXEC FUNCTION
editfunction user_exec($shell,$cmd) {
Ok...remember when our user_exec function was called above...this is what is being called. We passed 2 parameters to it, the $shell stream and the command ($cmd)
Writing To The Shell Stream
edit fwrite($shell,$cmd . PHP_EOL); //write to the shell
Remember when I said you can read from streams? Guess what!!! You can write to them too!!! fwrite does just that! So we are writing...to out $shell stream, the command and PHP_EOL. PHP_EOL stands for the end of line (equivalent of you pressing enter on your keyboard). I *think* you can use \r and \n instead, but somebody reccomended using PHP_EOL instead...Can't hurt (can somebody explain why?)
A few Local Variables
edit $output = ""; //will store our output
$start = false; //have we started yet
$output stores our output, and start says if we have reached [start] yet..but more on that below.
Timing the Loop: Why It's Important
edit $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
Important: Say your command was to run a shell script. And it runs for...I dunno 3 seconds. This is why while($line = fgets($shell)) won't work as intended.
Say we did use while($line = fgets($shell)...here is what will happen: 1) We started the SSH connection. Yay... 2) We got the shell stream... Yay 3) We waited a few milliseconds for the weird headers to load. 4) We wrote to the shell our command saying to run the shell script. Our big shell script is running. Remember it takes 2 seconds to run. That's a loot of time to php. 4) It starts reading in a loop. Every time fgets($shell) runs it spits out the next line. So we got through our headers in say... a half a second at the most. The strip we started is 1/4 done. Then we read the next line. UHOH!!! It doesn't exists O.o. The script hasn't outputted anything yet! Well, PHP thinks...this geezer told me to keep running fgets while it returns something. Looks like we're done here. And it exists out of our while loop 5) The shell script is still running for another 1 3/4 seconds. It screams out the output! But php doesn't hear it. So it's as if it never gave that output. Remember that saying "if a tree falls in a forest and nobody hears it...does it really make a sound?". Apparently it doesn't :) (Insert me laughing @ own corny joke!!!) Hope that example + corny joke helped to explain why the while() loop won't work in the case of running big scripts, so you might be wondering...well...then how DO we get the damn shell script's output. Here's how...
Timing the Loop
edit $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
It's pretty smart but also pretty simple. A user below suggested it. Here is how it works. time() gets the unix timestamp (# of seconds since the start of UNIX epoch). Huh? What? Don't worry. We just care that it's the time in seconds. If you do want to know more about time just lookup the time() function. It's...usefull... $max_time stores the maximum amount of time the loop can run.
In Depth
editwhile(((time()-$start_time) < $max_time)) { //if the x seconds haven't passed
Here's where the magic happens. Every time php does an iteration of the while loop it checks for a certain condition. The condition is that the max time has not elapsed yet. Here's how it knows Remember time()? Spits out the current time in seconds since the UNIX epoch started. Well...unsurprisingly if you called time() a minute ago and called it now, the current result for time() will be bigger... 60 bigger. Basic basic math you learned in elementary school. time() counts off the seconds from a certain date (January 1, 1970 to be exact). It will produce a whopping big number. But it doesn't matter all we care about is how many seconds the START number is less than the NOW number. That's what time()-$start_time does! Until that number reaches the max seconds we'll keep running the loop...over and over and over...It's gonna be quite a lot of iterations...but it doesn't matter.
Timing the Script: Why a Timeout *******
editNow here is the SUPER IMPORTANT PART You must make sure that the script you are trying to run will run in less than the amount specified in $max_time. Try running the script yourself and add an extra second or two based on the amount of time it took. You might be wondering why not make the $max_time something really really big like... 1000000000000000000. Then my script will have plenty of time to run. Here's the issue...If something goes wrong in your shell script and it doesn't quit for some reason, or produce output, php doesn't know...It will think it's still running. And it will run in a near-infinite loop until your 10000000000000000 seconds have passed. That's....not good. Not good at all....So make it a *reasonable* amount.
Getting the Next Line
edit $line = fgets($shell); //get the next line of output
we get the next line in our output and put it into line. So the first few time it will be the headers and the command we just gave. if our big script is still running it's keep trying to get the next line...and failing. over..and over and over. but we don't care. eventually the script will produce some output...
Making Sure Our Command Isn't Included in Output
edit if(!stristr($line,$cmd)) { //we don't want output that was out command (because it also contains [start] and [end]
Alright, we only care about the output of our command, not all the other random crap that happens in the terminal. That's why we included echo [start] which will print out a literal [start] onto the screen telling php (below) to start looking at the output. But WAIT!!! The command we used to do that was echo '[start]'.......that contains [start] :P!!!! stristr as I explained before looks for the second parameter in the first. So if it find our command...we don't want it. Otherwise we keep going (note ! means NOT). Usefull operator
Checking for Start of Line
edit if(preg_match('/\[start\]/',$line)) { //if we see that [start] is in the line that means we started
preg_match matches parameter a in parameter b.. Kinda like stristr except backwards and it uses regular expressions (you can read up on those on http://www.regular-expressions.info/ They are really useful but...too advanced for this n00b-oriented tutorial). If you wanna know more email me (hmm....just got an idea...maybe I should make a site explaining advanced concepts to n00bs....)
$start = true; //set start to true
}
Basically, if we match [start] we don't want that in our output, but we want to tell php that everything after this point is output. So we set the boolean $start to true.
elseif(preg_match('/\[end\]/',$line)) { //we're done
return $output; //return what we have (last line)
}
if it matches [end], that means it's time to stop. So it returns our output, stored in ($output) which breaks the loop. this is stored in whatever variable we sent the result of user_exec() to.
elseif($start && isset($line) && $line != "")
{
Well, if it wasn't the start or the end, then it could one of two things: the output we want, or the output we don't. That's that $start is for. Think back to our first if statement. If it saw [start], it set $start to true. otherwise it's false. if you remember how booleans and conditions work, the && operator means to make sure that both ___ and ___ are true. so here both $start and isset($line) and $line != "" have to yeild TRUE. If $start was set to true by our top if block yay we go on. isset() is a functions that returns true if a certain variable... is set. Now remember our poor $line variable. For all those times that the shell script is running it isn't getting set to anything because fgets($shell) isn't returning anything!!! So basically isset($line) will check for that. If not, then well...time to restart the loop. over and over and over until something finally gets into $line. and $line != "" returns true if $line is NOT a blank line. We don't want blank lines...or do we...up to you. I don't for my purposes. But now you know what to do if you do.
$output = $line; //return only last line (.= for all lines)
So say all 3 of our conditions are matched. There was a [start] before this line, $line is set to something, and it's not blank.
Important: What happens here is it resets $output to the contents of the latest line every time it runs. That's what I want. I only want the last line. It will keep replacing $output, line by line, until we read [end] in which case $output will only hold the last line. If you want ALL the output, you can do one of two things
1) Do $output .= $line . "
"; <- this is in the case that you are spitting out the output on screen. You APPEND (.=) to $output the contents of line and a break.
2) Do $output[] = $line; <- this adds the line to an array. So that you can analyze it further through php
That's it! Now you should understand how ever part of this script works, how to edit it, and more. Even if you're a beginner in PHP. Wasn't that easy? If you still don't get something you can email me at adz@jewc.org. It's my first time doing such a super-ultra-detailed tutorial, so I want your input!!!