Advertisement
Guest User

Untitled

a guest
Mar 30th, 2020
283
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 27.65 KB | None | 0 0
  1. <?php
  2.  
  3. namespace TelnetClient;
  4.  
  5. require_once('AnsiAsciiControlParser.php');
  6.  
  7. use \AnsiAsciiControlParser;
  8.  
  9.  
  10. class UnimplementedException extends \ErrorException {
  11. }
  12.  
  13. class NameResolutionException extends \InvalidArgumentException {
  14. }
  15.  
  16. class ConnectionException extends \RuntimeException {
  17. }
  18.  
  19. class ConnectionTimeoutException extends ConnectionException {
  20. }
  21.  
  22. class LoginException extends \RuntimeException {
  23. }
  24.  
  25. class UnlikelyException extends \ErrorException {
  26. }
  27.  
  28. error_reporting(E_ALL ^ E_WARNING);
  29.  
  30. class TelnetClient {
  31. /* NVT special characters
  32. * specified in the same order as in RFC854
  33. * same name as there, with a NVT_ prefix (to avoid clash with PHP keywords)
  34. */
  35.  
  36. // Codes that have special meaning to the NVT Printer
  37. const NVT_NUL = "\x00";
  38. const NVT_LF = "\n"; //"\x0A";
  39. const NVT_CR = "\r"; //"\x0D";
  40.  
  41. const NVT_BEL = "\x07";
  42. const NVT_BS = "\x08";
  43. const NVT_HT = "\x09";
  44. const NVT_VT = "\x0B";
  45. const NVT_FF = "\x0C";
  46.  
  47. /* TELNET command characters
  48. * "Note that these codes and code sequences have the indicated meaning
  49. * only when immediately preceded by an IAC." RFC854
  50. */
  51. /* RFC1123:
  52. * MUST: SE, NOP, DM, IP, AO, AYT, SB
  53. * SHOULD: EOR, EC, EL, BRK
  54. */
  55. const CMD_SE = "\xF0"; //Subnegotiation End
  56. const CMD_NOP = "\xF1";
  57. const CMD_DM = "\xF2"; //Data Mark
  58. const CMD_BRK = "\xF3"; //Break
  59. const CMD_IP = "\xF4"; //Interrupt Process
  60. const CMD_AO = "\xF5"; //Abort Output
  61. const CMD_AYT = "\xF6"; //Are You There
  62. const CMD_EC = "\xF7"; //Erase Character
  63. const CMD_EL = "\xF8"; //Erase Line
  64. const CMD_GA = "\xF9"; //Go Ahead
  65. const CMD_SB = "\xFA"; //Subnegotiation (start)
  66. const CMD_WILL = "\xFB";
  67. const CMD_WONT = "\xFC";
  68. const CMD_DO = "\xFD";
  69. const CMD_DONT = "\xFE";
  70. const CMD_IAC = "\xFF"; //Interpret As Command
  71.  
  72. const OPT_TXBIN = "\x00"; //Transmit binary, RFC856
  73. const OPT_ECHO = "\x01"; //Echo, RFC857
  74. const OPT_SGA = "\x03"; //Suppress Go Ahead, RFC858 (makes connection full-duplex instead of half-duplex)
  75. const OPT_STATUS = "\x05"; //Status, RFC859
  76. const OPT_TIMMRK = "\x06"; //Timing Mark, RFC860
  77. const OPT_EXTOPL = "\xFF"; //Extended options list, RFC861
  78. const OPT_EOR = "\x19"; //25, End of record, RFC885
  79. const OPT_3270_R = "\x1D"; //29, 3270 Regimes (?), RFC1041
  80. const OPT_NAWS = "\x1F"; //31, Negotiate About Window Size, RFC1073
  81. const OPT_TERMSPD = "\x20"; //32, Terminal speed, RFC1079
  82. const OPT_TERMTYP = "\x18"; //24, Terminal type, RFC1091
  83. const OPT_XDISPLOC = "\x23"; //35, X Display Location, RFC1096
  84. const OPT_LINEMODE = "\x22"; //34, Linemode, RFC1116
  85. const OPT_NEW_ENV = "\x27"; //39, New environment variable, RFC1572
  86. const OPT_R_FLW_CTR = "\x21"; //33, Remote Flow Control, RFC1080
  87.  
  88. const STATE_DEFAULT = 0;
  89. const STATE_CMD = 1;
  90.  
  91. const TELNET_ERROR = false;
  92. const TELNET_OK = true;
  93.  
  94.  
  95. private static $DEBUG = false;
  96.  
  97. private static $NVTP_SPECIALS = array(
  98. self::NVT_NUL => 'NUL',
  99. self::NVT_LF => 'LF',
  100. self::NVT_CR => 'CR',
  101. self::NVT_BEL => 'BEL',
  102. self::NVT_BS => 'BS',
  103. self::NVT_HT => 'HT',
  104. self::NVT_VT => 'VT',
  105. self::NVT_FF => 'FF',
  106. );
  107.  
  108. private static $CMDS = array(
  109. self::CMD_SE => 'SE',
  110. self::CMD_NOP => 'NOP',
  111. self::CMD_DM => 'DM',
  112. self::CMD_BRK => 'BRK',
  113. self::CMD_IP => 'IP',
  114. self::CMD_AO => 'AO',
  115. self::CMD_AYT => 'AYT',
  116. self::CMD_EC => 'EC',
  117. self::CMD_EL => 'EL',
  118. self::CMD_GA => 'GA',
  119. self::CMD_SB => 'SB',
  120. self::CMD_WILL => 'WILL',
  121. self::CMD_WONT => 'WONT',
  122. self::CMD_DO => 'DO',
  123. self::CMD_DONT => 'DONT',
  124. self::CMD_IAC => 'IAC'
  125. );
  126.  
  127. private static $OPTS = array(
  128. self::OPT_TXBIN => 'Transmit Binary',
  129. self::OPT_ECHO => 'Echo',
  130. self::OPT_SGA => 'Suppress Go Ahead',
  131. self::OPT_STATUS => 'Status',
  132. self::OPT_TIMMRK => 'Timing Mark',
  133. self::OPT_EXTOPL => 'Extended Options List',
  134. self::OPT_EOR => 'End Of Record',
  135. self::OPT_3270_R => '3270-Regime',
  136. self::OPT_NAWS => 'Negotiate About Window Size',
  137. self::OPT_TERMSPD => 'Terminal Speed',
  138. self::OPT_TERMTYP => 'Terminal Type',
  139. self::OPT_XDISPLOC => 'X Display Location',
  140. self::OPT_LINEMODE => 'Linemode',
  141. self::OPT_NEW_ENV => 'New Environment',
  142. self::OPT_R_FLW_CTR => 'Remote Flow Control'
  143. );
  144.  
  145.  
  146. private $host;
  147. private $ip_address;
  148. private $port;
  149. private $connect_timeout; //Timeout to connect to remote
  150. private $socket_timeout; //Timeout to wait for data
  151. private $full_line_timeout; //Timeout to wait for a full line
  152. private $state;
  153. private $do_get_remaining_data;
  154. private $socket;
  155. private $regex_prompt;
  156. private $errno;
  157. private $errstr;
  158. private $has_go_ahead;
  159. private $ctrlcharparser;
  160. private $pruneCtrlSeq;
  161.  
  162.  
  163. /**
  164. * Toggle debugging for code defined in this class only
  165. *
  166. * Ideally, this would work if subclasses
  167. * defined only their own static $DEBUG field, but the parent
  168. * class doesn't have access to children's private fields (unsurprisingly)
  169. *
  170. * Therefore, child classes need to define both their own $DEBUG field AND
  171. * copy this method. This seems to be the cleanest way to do it as
  172. * it breaks cleanly if a child class doesn't do it
  173. */
  174. public static function setDebug($enable) {
  175. static::$DEBUG = !!$enable;
  176. }
  177.  
  178.  
  179. /**
  180. * Translates codes to user-readable strings.
  181. *
  182. * @param string $code the code to translate
  183. * @param string[] $CODE_LIST an array of $code => string
  184. *
  185. * @return string A user-readable string ("0x<hexstring>" is returned for unknown keys)
  186. */
  187. private static function getCodeStrOrHexStr($code, $CODE_LIST) {
  188. if (array_key_exists($code, $CODE_LIST)) {
  189. return $CODE_LIST[$code];
  190. }
  191.  
  192. return '0x' . bin2hex($code);
  193. }
  194.  
  195.  
  196. /**
  197. * Get a user-readable string for NVT special characters
  198. *
  199. * Example: getNvtPrintSpecialStr(TelnetClient::NVT_NUL) prints "NUL"
  200. *
  201. * @param string the code to translate
  202. * @return string a printable representation of the code ("0x<hexstring> is returned for unknown codes)
  203. */
  204. public static function getNvtPrintSpecialStr($code) {
  205. return self::getCodeStrOrHexStr($code, self::$NVTP_SPECIALS);
  206. }
  207.  
  208.  
  209. /**
  210. * Get a user-readable string for TELNET command characters
  211. *
  212. * Example: getNvtPrintSpecialStr(TelnetClient::CMD_IAC)
  213. * Note: The user-readable strings are not guaranteed to be stable
  214. *
  215. * @param string the code to translate
  216. * @return string a printable representation of the code ("0x<hexstring> is returned for unknown codes)
  217. */
  218. public static function getCmdStr($code) {
  219. return self::getCodeStrOrHexStr($code, self::$CMDS);
  220. }
  221.  
  222.  
  223. /**
  224. * Get a user-readable string for TELNET option characters
  225. *
  226. * Example: getNvtPrintSpecialStr(TelnetClient::OPT_ECHO)
  227. * Note: The user-readable strings are not guaranteed to be stable
  228. *
  229. * @param string the code to translate
  230. * @return string a printable representation of the code ("0x<hexstring> is returned for unknown codes)
  231. */
  232. public static function getOptStr($code) {
  233. return self::getCodeStrOrHexStr($code, self::$OPTS);
  234. }
  235.  
  236.  
  237. /**
  238. * Constructor. Initialises host, port and timeout parameters
  239. * defaults to localhost port 23 (standard telnet port)
  240. *
  241. * @param string $host Host name or IP addres
  242. * @param int $port TCP port number
  243. * @param float $connect_timeout the timeout for connecting to the host
  244. * @param string $prompt the default prompt
  245. * @param float|null $socket_timeout the timeout to wait for new data (null = infinite)
  246. * @throws \InvalidArgumentException if an argument is invalid
  247. */
  248. public function __construct($host = '127.0.0.1', $port = 23, $connect_timeout = 1.0, $socket_timeout = 10.0, $prompt = '$', $full_line_timeout = 0.10) {
  249. $this->host = $host;
  250.  
  251. $this->port = $port;
  252.  
  253. $this->setConnectTimeout($connect_timeout);
  254. $this->setSocketTimeout($socket_timeout);
  255. $this->setFullLineTimeout($full_line_timeout);
  256. $this->setPrompt($prompt);
  257.  
  258. $this->state = self::STATE_DEFAULT;
  259. $this->do_get_remaining_data = true;
  260. $this->has_go_ahead = false; //By default the server speaks first (?)
  261. $this->cntrlcharparser = new AnsiAsciiControlParser();
  262. $this->pruneCtrlSeq = false;
  263. }
  264.  
  265.  
  266. /**
  267. * Destructor. Cleans up socket connection and command buffer
  268. *
  269. * @return void
  270. */
  271. public function __destruct() {
  272. // clean up resources
  273. $this->disconnect();
  274. }
  275.  
  276.  
  277. /**
  278. * Attempts connection to remote host.
  279. *
  280. * @return boolean true if successful
  281. * @throws NameResolutionException on error
  282. * @throws ConnectionException on error
  283. */
  284. public function connect() {
  285. $this->ip_address = gethostbyname($this->host);
  286.  
  287. if (filter_var($this->ip_address, FILTER_VALIDATE_IP) === false) {
  288. throw new NameResolutionException("Cannot resolve $this->host");
  289. }
  290.  
  291. // attempt connection - suppress warnings
  292. $this->socket = @fsockopen($this->ip_address, $this->port, $this->errno, $this->errstr, $this->connect_timeout);
  293. if ($this->socket === false) {
  294. throw new ConnectionException("Cannot connect to $this->host on port $this->port (errno={$this->errno} errstr='{$this->errstr}')");
  295. }
  296. stream_set_blocking($this->socket, 0);
  297.  
  298. return self::TELNET_OK;
  299. }
  300.  
  301.  
  302. /**
  303. * Closes IP socket
  304. *
  305. * @return boolean
  306. * @throws UnlikelyException if closing the socket failed
  307. */
  308. public function disconnect() {
  309. if (is_resource($this->socket)) {
  310. if (fclose($this->socket) === false) {
  311. throw new UnlikelyException("Error while closing telnet socket");
  312. }
  313. $this->socket = null;
  314. }
  315. return self::TELNET_OK;
  316. }
  317.  
  318.  
  319. /**
  320. * @deprecated please use setSocketTimeout($socket_timeout)
  321. */
  322. public function setStreamTimeout($socket_timeout) {
  323. return $this->setSocketTimeout($socket_timeout);
  324. }
  325.  
  326.  
  327. /**
  328. * @param float|null $socket_timeout the timeout to wait for new data (null = infinite)
  329. * @return void
  330. * @throws \InvalidArgumentException if $socket_timeout is of the wrong type or value
  331. */
  332. public function setSocketTimeout($socket_timeout) {
  333. if (!(is_null($socket_timeout)
  334. || ((is_float($socket_timeout) && $socket_timeout >= 0.0)))) {
  335. throw new \InvalidArgumentException('socket_timeout must be non-negative float or null');
  336. }
  337. $this->socket_timeout = $socket_timeout;
  338. }
  339.  
  340.  
  341. /**
  342. * Returns the time, in seconds, to wait between characters (when waiting for new data) before timing out
  343. *
  344. * @return float|null the current socket timeout (null = infinite)
  345. */
  346. public function getSocketTimeout() {
  347. return $this->socket_timeout;
  348. }
  349.  
  350.  
  351. /**
  352. * @return float|null the timeout for a full line
  353. * @see setFullLineTimeout()
  354. */
  355. public function getFullLineTimeout() {
  356. return $this->full_line_timeout;
  357. }
  358.  
  359.  
  360. /**
  361. * @param float|null $full_line_timeout The maximum time to wait for before
  362. * assuming the line is not carriage return terminated. null for infinity
  363. *
  364. * Note: Setting a timeout value larger than socket timeout is probably a bad idea
  365. */
  366. public function setFullLineTimeout($full_line_timeout) {
  367. if (!(is_null($full_line_timeout)
  368. || (is_float($full_line_timeout) && $full_line_timeout >= 0.0))) {
  369. throw new \InvalidArgumentException('full_line_timeout must be null or non-negative float');
  370. }
  371. $this->full_line_timeout = $full_line_timeout;
  372. }
  373.  
  374.  
  375. public function getPruneCtrlSeq() {
  376. return $this->pruneCtrlSeq;
  377. }
  378.  
  379.  
  380. public function setPruneCtrlSeq($enable) {
  381. $this->pruneCtrlSeq = !!$enable;
  382. }
  383.  
  384.  
  385. /**
  386. * @return string the hostname (as passed to the ctor) to connect to
  387. */
  388. public function getHostname() {
  389. return $this->host;
  390. }
  391.  
  392.  
  393. /**
  394. * @return string the IP address the hostname was last resolved to
  395. */
  396. public function getIpAddress() {
  397. return $this->ip_address;
  398. }
  399.  
  400.  
  401. /**
  402. * @return float the connect timeout
  403. * @see self::setConnectTimeout()
  404. */
  405. public function getConnectTimeout() {
  406. return $this->connectTimeout;
  407. }
  408.  
  409.  
  410. /**
  411. * @param float $connect_timeout the timeout, in seconds, to wait for the socket connection/TCP handshake
  412. * @return void
  413. * @throws \InvalidArgumentException if $connect_timeout is not float or is negative
  414. */
  415. public function setConnectTimeout($connect_timeout) {
  416. if (!(is_float($connect_timeout) && $connect_timeout >= 0.0)) {
  417. throw new \InvalidArgumentException('connect_timeout must be float');
  418. }
  419. $this->connect_timeout = $connect_timeout;
  420. }
  421.  
  422.  
  423. /**
  424. * Executes command and returns a string with result.
  425. *
  426. * @param string $command Command to execute
  427. * @param boolean $add_newline Default true, adds newline to the command
  428. * @return string Command result
  429. */
  430. public function exec($command, $add_newline = true) {
  431. $this->sendCommand($command, $add_newline);
  432. $a_line = $this->waitPrompt($this->do_get_remaining_data);
  433.  
  434. return $a_line;
  435. }
  436.  
  437.  
  438. public function sendCommand($command, $add_newline = true) {
  439. //TODO: Pass $command into the state machine to escape IACs, also look at UTF-8 RFC about how to escape newlines
  440. $this->write($command, $add_newline);
  441. }
  442.  
  443.  
  444. /**
  445. * @param boolean $matchesPrompt reference set to true if the line matches the prompt, false otherwise
  446. * @param boolean $waitForFullLine true to wait indefinitely for a full
  447. * line ("\n" terminated). False waits for up to fullLineTimeout before returning
  448. *
  449. * @return string|false The line as a string with its terminating "\n".
  450. * In "non-full line" mode, the line may be incomplete or false (if no character is available)
  451. */
  452. public function getLine(&$matchesPrompt, $waitForFullLine = true) {
  453. $waitForFullLine = !!$waitForFullLine;
  454.  
  455. do {
  456. $line = $this->getNextLine();
  457. } while ($waitForFullLine && $line === false);
  458. $matchesPrompt = $this->matchesPrompt($line);
  459. return $line;
  460. }
  461.  
  462.  
  463. /**
  464. * Discards unread but received data, processing options if any
  465. */
  466. public function discardRemainingData() {
  467. $this->getRemainingData();
  468. }
  469.  
  470.  
  471. /**
  472. * Attempts login to remote host.
  473. *
  474. * @param string $username Username
  475. * @param string $password Password
  476. * @param string|null $login_prompt the string to look for before sending the username (null to skip)
  477. * @param string|null $password_prompt the string to look for before sending the password (null to skip)
  478. * @return boolean
  479. * @throws LoginException on error
  480. */
  481. public function login($username, $password, $login_prompt = 'login:', $password_prompt = 'Password:') {
  482. $prompt = $this->regex_prompt;
  483. try {
  484. if (!is_null($login_prompt)) {
  485. $this->setPrompt($login_prompt);
  486. $this->waitPrompt($this->do_get_remaining_data);
  487. $this->write($username);
  488. }
  489.  
  490. if (!is_null($password_prompt)) {
  491. $this->setPrompt($password_prompt);
  492. $this->waitPrompt($this->do_get_remaining_data);
  493. $this->write($password);
  494. }
  495.  
  496. //Reset prompt
  497. $this->regex_prompt = $prompt;
  498.  
  499. $this->waitPrompt($this->do_get_remaining_data);
  500. } catch (\Exception $e) {
  501. throw new LoginException("Login failed", 0, $e);
  502. }
  503.  
  504. return self::TELNET_OK;
  505. }
  506.  
  507.  
  508. /**
  509. * @param boolean $enable true if all remaining data is to be fetched after the prompt is found, false otherwise
  510. * @return void
  511. */
  512. public function setDoGetRemainingData($enable) {
  513. $this->do_get_remaining_data = !!$enable;
  514. }
  515.  
  516.  
  517. /**
  518. * @return boolean true if all remaining data is to be fetched after the prompt is found, false otherwise
  519. */
  520. public function getDoGetRemainingData() {
  521. return $this->do_get_remaining_data;
  522. }
  523.  
  524.  
  525. /**
  526. * Sets the string of characters to respond to.
  527. * This should be set to the last character of the command line prompt
  528. *
  529. * @param string $str String to respond to
  530. * @return boolean true on success
  531. */
  532. public function setPrompt($str = '$') {
  533. return $this->setRegexPrompt(preg_quote($str, '/'));
  534. }
  535.  
  536.  
  537. /**
  538. * @return string the current regex prompt without the enclosing
  539. * slashes and $ metacharacter
  540. * @see self::setRegexPrompt()
  541. */
  542. public function getRegexPrompt() {
  543. return $this->regex_prompt;
  544. }
  545.  
  546.  
  547. /**
  548. * Sets a regex string to respond to.
  549. * This should be set to the last line of the command line prompt.
  550. *
  551. * Note: The actual regex is "/{$str}$/", so you will need to escape slashes
  552. * and must not include a $ metacharacter
  553. *
  554. * @param string $str Regex string to respond to
  555. * @return boolean true on success
  556. * @throws \InvalidArgumentException if the regex doesn't compile
  557. */
  558. public function setRegexPrompt($str = '\$') {
  559. $this->regex_prompt = $str;
  560.  
  561. if (false === preg_match("/{$this->regex_prompt}$/", '')) {
  562. throw new \InvalidArgumentException('Malformed PCRE error');
  563. }
  564.  
  565. return self::TELNET_OK;
  566. }
  567.  
  568.  
  569. private function asyncGetc() {
  570. $c = fgetc($this->socket);
  571. return $c;
  572. }
  573.  
  574.  
  575. /**
  576. * @deprecated please use waitForNbData(1);
  577. * Note: This function doesn't add the returned characters to the buffer nor the global buffer
  578. */
  579. protected function getc() {
  580. $c = $this->waitForNbData(1);
  581. return $c;
  582. }
  583.  
  584.  
  585. /**
  586. * Reads up to $length bytes of data (TELNET commands are not counted) or wait for $this->socket_timeout seconds, whichever occurs first
  587. *
  588. * @param int|null $length maximum number of data bytes to read. Either a non-negative int or null (infinite length)
  589. *
  590. * @return string the raw data read as a string
  591. * @throws \InvalidArgumentException if $length is neither null nor int
  592. * @throws \InvalidArgumentException if $length is int and smaller than 1
  593. * @throws \InvalidArgumentException if $length is null and socket_timeout is null
  594. */
  595. private function waitForNbData($length = null) {
  596. if (is_null($length) && is_null($this->socket_timeout)) {
  597. throw new \InvalidArgumentException('Would wait infinitely');
  598. } else if (!is_null($length) && (!is_int($length) || $length < 1)) {
  599. throw new \InvalidArgumentException('$length must be a positive int');
  600. }
  601.  
  602. $cb = function ($nbchar, $c, $length) {
  603. //FIXME: This is wrong: if we get IAC in the middle, we will never return
  604. return is_null($length) || $nbchar < $length;
  605. };
  606.  
  607. return $this->getMoreData($cb, $length);
  608. }
  609.  
  610.  
  611. private function _getNextLine_cb($nbchar, $c, &$firstCharTs) {
  612. if (is_null($firstCharTs)) {
  613. if ($c !== false) {
  614. $firstCharTs = microtime(true);
  615. }
  616. } else if ($c === "\n") {
  617. return false;
  618. } else if (!is_null($this->full_line_timeout) && $firstCharTs + $this->full_line_timeout <= microtime(true)) {
  619. return false;
  620. }
  621.  
  622. //if ($c !== false) {
  623. // var_dump(microtime(true));
  624. // var_dump($c);
  625. //}
  626. return $c !== "\n";
  627. }
  628.  
  629.  
  630. /**
  631. */
  632. private function getNextLine() {
  633. $line = $this->getMoreData(array($this, '_getNextLine_cb'));
  634.  
  635. if ($line !== false && $this->pruneCtrlSeq) {
  636. $this->cntrlcharparser->parse($line);
  637. $line = $this->cntrlcharparser->getTextString();
  638. }
  639.  
  640. return $line;
  641. }
  642.  
  643.  
  644. private function getRemainingData() {
  645. $cb = function ($nbchar, $c, $userData) {
  646. return $c !== false;
  647. };
  648.  
  649. $data = $this->getMoreData($cb);
  650.  
  651. if ($data !== false && $this->pruneCtrlSeq) {
  652. $this->cntrlcharparser->parse($data);
  653. $data = $this->cntrlcharparser->getTextString();
  654. }
  655.  
  656. return $data;
  657. }
  658.  
  659.  
  660. /**
  661. * @param callable $get_more_data_cb boolean get_more_data_cb(numberOfCharactersInTheArray, lastCharacter, userData), $c will be false if no character is no more characters are available at the moment
  662. * @param mixed $userData a user parameter passed to the callable
  663. * @throws ConnectionTimeoutException
  664. *
  665. * @return string|false the data received or false if none
  666. */
  667. private function getMoreData($get_more_data_cb, $userData = null) {
  668. $data = false;
  669. $endTs = microtime(true) + $this->socket_timeout;
  670. $a_c = array();
  671. $c = null;
  672. $is_get_more_data = null;
  673. do {
  674. $c = $this->asyncGetc();
  675. if ($c === false) {
  676. usleep(5);
  677. if (!is_null($this->socket_timeout) && microtime(true) > $endTs) {
  678. throw new ConnectionTimeoutException("Timed out");
  679. }
  680. continue;
  681. } else {
  682. //Reset the timeout
  683. $endTs = microtime(true) + $this->socket_timeout;
  684. }
  685. $a_c[] = $c;
  686.  
  687. $is_get_more_data = $this->processStateMachine($a_c);
  688. if (!$is_get_more_data && count($a_c) > 0) {
  689. if ($data === false) {
  690. $data = '';
  691. }
  692. $new_data = implode($a_c);
  693. if (self::$DEBUG) {
  694. print("Adding " . (ctype_print($new_data) ? "\"{$new_data}\"" : "(0x" . bin2hex($new_data) . ")") . " to buffer\n");
  695. //print("Adding \"{$new_data}\" (0x" . bin2hex($new_data) . ") to buffer (count = " . count($a_c) . " len = " . strlen($new_data) . ")\n");
  696. //var_dump($a_c);
  697. }
  698. $data .= $new_data;
  699. $a_c = array();
  700. }
  701. } while ($is_get_more_data || call_user_func_array($get_more_data_cb, array(count($data), $c, &$userData)));
  702.  
  703. return $data;
  704. }
  705.  
  706.  
  707. /**
  708. * This function processes the stream received (passed as an array of NVT characters) and filters TELNET protocol data out.
  709. * It is meant to be called once each time a new character is added to the array. The array can only be said to contain data once the return code is false
  710. *
  711. * @param array $a_c array of characters to process.
  712. * @return boolean true if more characters are needed, false if processing is done ($a_c was cleaned of TELNET protocol data such that it contains only actual data)
  713. * @throws UnimplementedException on unknown state
  714. */
  715. private function processStateMachine(array &$a_c) {
  716. $is_get_more_data = false;
  717.  
  718. switch ($this->state) {
  719. case self::STATE_DEFAULT:
  720. $is_get_more_data = $this->processStateMachineDefaultState($a_c);
  721. break;
  722. case self::STATE_CMD:
  723. $is_get_more_data = $this->processStateMachineCmdState($a_c);
  724. break;
  725. //case self::STATE_BINARY:
  726. // break;
  727. //case self::STATE_OPT:
  728. // break;
  729. //case self::STATE_NEG_NO:
  730. // break;
  731. //case self::STATE_NEG_YES:
  732. // break;
  733. default:
  734. throw new UnimplementedException("Unimplement state {$this->state}");
  735. break;
  736. }
  737.  
  738. return $is_get_more_data;
  739. }
  740.  
  741.  
  742. /**
  743. * Processes the default state, should only be called from processStateMachine()
  744. *
  745. * @param array $a_c array of characters to process.
  746. * @return boolean true if more characters are needed, false if processing is done ($a_c was cleaned of TELNET protocol data such that it contains only actual data)
  747. */
  748. private function processStateMachineDefaultState(array &$a_c) {
  749. $is_get_more_data = false;
  750.  
  751. switch ($a_c[0]) {
  752. case self::CMD_IAC:
  753. if (count($a_c) < 2) {
  754. $is_get_more_data = true;
  755. break;
  756. }
  757. $cmd = $a_c[1];
  758. if ($cmd === self::CMD_IAC) {
  759. /* Is this supposed to happen in normal mode? (Yes,
  760. * "With the current set-up, only the IAC need be doubled to be sent as data" --RFC854) */
  761.  
  762. //Add (only) one IAC character to the data
  763. $is_get_more_data = false;
  764. $a_c = array(self::CMD_IAC);
  765.  
  766. } else {
  767. $is_get_more_data = true;
  768. $this->state = self::STATE_CMD;
  769. }
  770. break;
  771.  
  772. case self::NVT_CR:
  773. if (count($a_c) < 2) {
  774. $is_get_more_data = true;
  775. } else {
  776. switch ($a_c[1]) {
  777. case self::NVT_LF:
  778. //Replace <CR> <LF> by "\n" (only in STATE_DEFAULT)
  779. $a_c = array("\n");
  780. break;
  781. }
  782. }
  783. break;
  784. default:
  785. //Pass, raw data
  786. }
  787.  
  788. return $is_get_more_data;
  789. }
  790.  
  791.  
  792. /**
  793. * Processes the command state, should only be called from processStateMachine()
  794. *
  795. * @param array $a_c array of characters to process.
  796. * @return boolean true if more characters are needed, false if processing is done ($a_c was cleaned of TELNET protocol data such that it contains only actual data)
  797. * @throws ConnectionException if sending command negotiation fails
  798. */
  799. private function processStateMachineCmdState(array &$a_c) {
  800. $is_get_more_data = false;
  801.  
  802. if (count($a_c) < 3) {
  803. //Get more data
  804. $is_get_more_data = true;
  805.  
  806. } else if ($a_c[0] !== self::CMD_IAC) {
  807. //Pass;
  808.  
  809. } else {
  810. $cmd = $a_c[1];
  811. $opt = $a_c[2];
  812. $reply_cmd = null;
  813. switch ($cmd) {
  814. case self::CMD_SB:
  815. if ($opt === self::CMD_SE) {
  816. //Empty subnegotiation?! (pass)
  817. } else if (end($a_c) !== self::CMD_SE) {
  818. //Get more data
  819. $is_get_more_data = true;
  820. } else {
  821. //TODO: Handle subnegotiation here
  822. if (self::$DEBUG) {
  823. print("Silently dropping subnegotiation (to be implemented)\n");
  824. }
  825. }
  826. break;
  827.  
  828. //TODO: Handle other commands
  829. case self::CMD_DO: //FALLTHROUGH
  830. case self::CMD_DONT:
  831. $reply_cmd = self::CMD_WONT;
  832. break;
  833.  
  834. case self::CMD_WILL:
  835. $reply_cmd = self::CMD_DONT;
  836. break;
  837. case self::CMD_WONT:
  838. //Pass, we are not supposed to "acknowledge" WONTs
  839. //TODO: Reread the Q method RFC, I don't remember if this is right
  840. break;
  841.  
  842. default:
  843. if (self::$DEBUG) {
  844. print('Ignoring unknown command ' . self::getCmdStr($cmd) . "\n");
  845. }
  846. }
  847.  
  848. if (!is_null($reply_cmd)) {
  849. $buffer = self::CMD_IAC . $reply_cmd . $opt;
  850. $ret = fwrite($this->socket, $buffer);
  851. if ($ret !== strlen($buffer)) { //|| $ret === false) {
  852. throw new ConnectionException("Error writing to socket");
  853. }
  854.  
  855. if (self::$DEBUG) {
  856. $str = sprintf("[CMD %s]", self::getCmdStr($cmd));
  857. $str .= sprintf("[OPT %s]", self::getOptStr($opt));
  858. print($str . "\n");
  859. }
  860. }
  861. if (!$is_get_more_data) {
  862. $a_c = array();
  863. //FIXME: Do we always return to the default state? Or is it possible to negotiate in binary mode for example?
  864. $this->state = self::STATE_DEFAULT;
  865. }
  866. }
  867.  
  868. return $is_get_more_data;
  869. }
  870.  
  871.  
  872. /**
  873. * Write command to a socket
  874. *
  875. * @param string $buffer Stuff to write to socket
  876. * @param boolean $add_newline Default true, adds newline to the command
  877. * @return boolean true on success
  878. * @throws ConnectionException if connection on socket errors
  879. */
  880. protected function write($buffer, $add_newline = true) {
  881. if (!is_resource($this->socket)) {
  882. throw new ConnectionException("Telnet connection closed");
  883. }
  884.  
  885. if ($add_newline) {
  886. $buffer .= self::NVT_CR . self::NVT_LF;
  887. }
  888.  
  889. /*
  890. * FIXME: This is dubious: why not rely on DO ECHO?
  891. * (Admittedly the original code WONT/DONT all options and my test servers don't respect DONT ECHO)
  892. */
  893. //FIXME: Allow toggling this
  894. //FIXME: This also gets a pass at the <CR> <LF> filtering, which is bad
  895. /* FIXME: This also doesn't respect the order of things:
  896. * since we return as soon as the prompt is found (regardless of whether or not more characters have been received or not), appending to the global buffer means those characters get written after (while they were received before)
  897. */
  898. //$this->global_buffer .= $buffer;
  899.  
  900. $ret = fwrite($this->socket, $buffer);
  901. if ($ret !== strlen($buffer)) { //|| $ret === false)
  902. throw new ConnectionException("Error writing to socket");
  903. }
  904.  
  905. return self::TELNET_OK;
  906. }
  907.  
  908.  
  909. public function matchesPrompt($line) {
  910. return preg_match("/{$this->regex_prompt}/", $line) === 1;
  911. }
  912.  
  913.  
  914. /**
  915. * Reads socket until prompt is encountered
  916. *
  917. * @param boolean $do_get_remaining_data set to true to read all received data after the prompt is found
  918. * @return string[] array of lines (trailing "\n" are stripped)
  919. */
  920. protected function waitPrompt($do_get_remaining_data = false) {
  921. if (self::$DEBUG) {
  922. print("\nWaiting for prompt \"{$this->regex_prompt}\"\n");
  923. }
  924.  
  925. $a_line = array();
  926. do {
  927. $line = $this->getNextLine();
  928.  
  929. if ($line === false) {
  930. usleep(1);
  931. continue;
  932. } else {
  933. $a_line[] = rtrim($line, "\n");
  934. }
  935.  
  936. } while (!$this->matchesPrompt($line));
  937.  
  938. if ($do_get_remaining_data) {
  939. $line = $this->getRemainingData();
  940. if ($line !== false) {
  941. $a_line = array_merge($a_line, explode("\n", $line));
  942. }
  943. }
  944.  
  945. return $a_line;
  946. }
  947. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement