<?php /** @noinspection ALL */

/**
 * Telnet class
 *
 * Used to execute remote commands via telnet connection
 * Usess sockets functions and fgetc() to process result
 *
 * All methods throw Exceptions on error
 *
 * Written by Dalibor Andzakovic <dali@swerve.co.nz>
 * Based on the code originally written by Marc Ennaji and extended by
 * Matthias Blaser <mb@adfinis.ch>
 */
class Telnet {

	private $host;
	private $port;
	private $timeout;

	private $socket = null;
	private $buffer = null;
	private $prompt;
	private $errno;
	private $errstr;

	private $NULL;
	private $DC1;
	private $WILL;
	private $WONT;
	private $DO;
	private $DONT;
	private $IAC;

	const TELNET_ERROR = false;
	const TELNET_OK = true;

	/**
	 * Constructor. Initialises host, port and timeout parameters
	 * defaults to localhost port 23 (standard telnet port)
	 *
	 * @param string $host Host name or IP addres
	 * @param string $port TCP port number
	 * @param int $timeout Connection timeout in seconds
	 * @throws Exception
	 */
	public function __construct( $host = '127.0.0.1', $port = '23', $timeout = 10 ) {

		$this->host = $host;
		$this->port = $port;
		$this->timeout = $timeout;

		// set some telnet special characters
		$this->NULL = chr( 0 );
		$this->DC1 = chr( 17 );
		$this->WILL = chr( 251 );
		$this->WONT = chr( 252 );
		$this->DO = chr( 253 );
		$this->DONT = chr( 254 );
		$this->IAC = chr( 255 );

		$this->connect();
	}

	/**
	 * Destructor. Cleans up socket connection and command buffer
	 *
	 * @return void
	 */
	public function __destruct() {

		// cleanup resources
		$this->disconnect();
		$this->buffer = null;
	}

	/**
	 * Attempts connection to remote host. Returns TRUE if sucessful.
	 *
	 * @return boolean
	 */
	public function connect() {

		// check if we need to convert host to IP
		if ( !preg_match( '/([0-9]{1,3}\\.){3,3}[0-9]{1,3}/', $this->host ) ) {

			$ip = gethostbyname( $this->host );

			if ( $this->host == $ip ) {

				throw new Exception( "Cannot resolve $this->host" );
			} else {
				$this->host = $ip;
			}
		}

		// attempt connection
		$this->socket = fsockopen( $this->host, $this->port, $this->errno, $this->errstr, $this->timeout );
		stream_set_timeout( $this->socket, $this->timeout );


		if ( !$this->socket ) {
			throw new Exception( "Cannot connect to $this->host on port $this->port" );
		}

		return self::TELNET_OK;
	}

	/**
	 * Closes IP socket
	 *
	 * @return boolean
	 */
	public function disconnect() {
		if ( $this->socket ) {
			if ( !fclose( $this->socket ) ) {
				throw new Exception( "Error while closing telnet socket" );
			}
			$this->socket = null;
		}

		return self::TELNET_OK;
	}

	/**
	 * Executes command and returns a string with result.
	 * This method is a wrapper for lower level private methods
	 *
	 * @param string $command Command to execute
	 * @return string Command result
	 */
	public function exec( $command ) {

		$this->write( $command );
		$this->waitPrompt();

		return $this->getBuffer();
	}

	/**
	 * Attempts login to remote host.
	 * This method is a wrapper for lower level private methods and should be
	 * modified to reflect telnet implementation details like login/password
	 * and line prompts. Defaults to standard unix non-root prompts
	 *
	 * @param string $username Username
	 * @param string $password Password
	 * @param string $login_prompt
	 * @param string $password_prompt
	 * @param string $prompt
	 * @return boolean
	 * @throws Exception
	 */
	public function login( $username, $password, $login_prompt = 'login:', $password_prompt = 'Password:', $prompt = '#' ) {

		try {
			$this->setPrompt( $login_prompt );
			$this->waitPrompt();
			$this->write( $username );
			$this->setPrompt( $password_prompt );
			$this->waitPrompt();
			$this->write( $password );
			$this->setPrompt( $prompt );
			$this->waitPrompt();
		} catch ( Exception $e ) {

			throw new Exception( "Login failed." );
		}

		return self::TELNET_OK;
	}

	/**
	 * Sets the string of characters to respond to.
	 * This should be set to the last character of the command line prompt
	 *
	 * @param string $s String to respond to
	 * @return boolean
	 */
	public function setPrompt( $s = '$' ) {
		$this->prompt = $s;

		return self::TELNET_OK;
	}

	/**
	 * Gets character from the socket
	 *
	 * @return bool|string|void
	 */
	private function getc() {
		return fgetc( $this->socket );
	}

	/**
	 * Clears internal command buffer
	 *
	 * @return void
	 */
	private function clearBuffer() {
		$this->buffer = '';
	}

	/**
	 * Reads characters from the socket and adds them to command buffer.
	 * Handles telnet control characters. Stops when prompt is ecountered.
	 *
	 * @param string $prompt
	 * @return boolean
	 */
	private function readTo( $prompt ) {

		if ( !$this->socket ) {
			throw new Exception( "Telnet connection closed" );
		}

		// clear the buffer
		$this->clearBuffer();

		do {

			$c = $this->getc();

			if ( $c === false ) {
				throw new Exception( "Couldn't find the requested : '" . $prompt . "', it was not in the data returned from server : '" . $this->buffer . "'" );
			}

			// Interpreted As Command
			if ( $c == $this->IAC ) {
				if ( $this->negotiateTelnetOptions() ) {
					continue;
				}
			}

			// append current char to global buffer
			$this->buffer .= $c;

			// we've encountered the prompt. Break out of the loop
			if ( strpos( $this->buffer, $prompt ) !== false ) {
				return self::TELNET_OK;
			}
		} while ( $c != $this->NULL || $c != $this->DC1 );
	}

	/**
	 * Write command to a socket
	 *
	 * @param string $buffer      Stuff to write to socket
	 * @param boolean $addNewLine Default true, adds newline to the command
	 * @return boolean
	 */
	private function write( $buffer, $addNewLine = true ) {

		if ( !$this->socket ) {
			throw new Exception( "Telnet connection closed" );
		}

		// clear buffer from last command
		$this->clearBuffer();

		if ( $addNewLine == true ) {
			$buffer .= "\n";
		}

		if ( !fwrite( $this->socket, $buffer ) < 0 ) {
			throw new Exception( "Error writing to socket" );
		}

		return self::TELNET_OK;
	}

	/**
	 * Returns the content of the command buffer
	 *
	 * @return string Content of the command buffer
	 */
	private function getBuffer() {
		// cut last line (is always prompt)
		$buf = explode( "\n", $this->buffer );
		unset( $buf[( count( $buf ) - 1 )] );
		$buf = implode( "\n", $buf );

		return trim( $buf );
	}

	/**
	 * Telnet control character magic
	 *
	 * @return boolean
	 * @throws Exception
	 */
	private function negotiateTelnetOptions() {

		$c = $this->getc();

		if ( $c != $this->IAC ) {

			if ( ( $c == $this->DO ) || ( $c == $this->DONT ) ) {

				$opt = $this->getc();
				fwrite( $this->socket, $this->IAC . $this->WONT . $opt );
			} else {
				if ( ( $c == $this->WILL ) || ( $c == $this->WONT ) ) {

					$opt = $this->getc();
					fwrite( $this->socket, $this->IAC . $this->DONT . $opt );
				} else {
					throw new Exception( 'Error: unknown control character ' . ord( $c ) );
				}
			}
		} else {
			throw new Exception( 'Error: Something Wicked Happened' );
		}

		return self::TELNET_OK;
	}

	/**
	 * Reads socket until prompt is encountered
	 */
	private function waitPrompt() {
		return $this->readTo( $this->prompt );
	}
}

?>