Advertisement
Guest User

Neufbox4 PHP class

a guest
Jan 11th, 2012
484
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 16.39 KB | None | 0 0
  1. <?php
  2. /**
  3.  * Provides access and control over Neufbox4 devices.
  4.  * Tested with firmware NB4-MAIN-R3.1.10.
  5.  *
  6.  * @require PHP > 5.1.2
  7.  * @author Anael Ollier <nanawel@gmail.com>
  8.  * @version 0.1.0
  9.  * @since 2011-12-25
  10.  */
  11. class Neufbox4 {
  12.     const CLASS_VERSION = '0.1.1';
  13.    
  14.     const DEFAULT_HOST = '192.168.1.1';
  15.    
  16.     const STATUS_CONNECTED = 0;
  17.     const STATUS_CONNECTING = 1;
  18.     const STATUS_UNUSED = 2;
  19.     const STATUS_NOT_CONNECTED = 3;
  20.    
  21.     const COOKIE_NAME = 'sid';
  22.     const REQUEST_TIMEOUT = 5;
  23.    
  24.     const LOG_DEBUG = 10;
  25.     const LOG_NOTICE = 20;
  26.     const LOG_WARNING = 30;
  27.     const LOG_ERROR = 40;
  28.    
  29.     /** @var string */
  30.     protected $_host;
  31.     /** @var string */
  32.     protected $_login;
  33.     /** @var string */
  34.     protected $_password;
  35.    
  36.     /** @var resource */
  37.     protected $_curl;
  38.     /** @var string */
  39.     protected $_sessionId;
  40.    
  41.     /** @var boolean */
  42.     public $debug = false;
  43.     /** @var int */
  44.     public $logLevel = self::LOG_DEBUG;
  45.    
  46.     /**
  47.      *
  48.      * @param string $host The Neufbox4 IP to connect to.
  49.      */
  50.     public function __construct($host = self::DEFAULT_HOST, $logLevel = self::LOG_NOTICE) {
  51.         $this->_host = $host;
  52.         $this->logLevel = $logLevel;
  53.         $this->_curl = curl_init($this->_getUrl('/'));
  54.         $this->log("Initialized new connection to host $host.");
  55.     }
  56.    
  57.     public function __destruct() {
  58.         curl_close($this->_curl);
  59.     }
  60.    
  61.     protected function _getUrl($path) {
  62.         return 'http://' . $this->_host . $path;
  63.     }
  64.    
  65.     /**
  66.      * Creates a new session with stored login/password if
  67.      * there's no current session.
  68.      *
  69.      * @param boolean $force
  70.      */
  71.     protected function _login($force = false) {
  72.         if ($force || !$this->_sessionId) {
  73.            
  74.             ///////////////////
  75.             // 1. Retrieve challenge (session ID)
  76.             $res = $this->_sendRawRequest('/login', 'post', array('action' => 'challenge'),
  77.                 array(
  78.                     'X-Requested-With: XMLHttpRequest',
  79.                     'X-Requested-Handler: ajax',
  80.                 )
  81.             );
  82.             if ($res === false) {
  83.                 self::throwException('Cannot log in: challenge request failed.');
  84.             }
  85.            
  86.             if (200 != ($code = $res['info']['http_code'])) {
  87.                 self::throwException("Cannot log in: unexpected code HTTP $code returned.");
  88.             }
  89.             elseif ('text/xml' != ($contentType = $res['info']['content_type'])) {
  90.                 self::throwException("Cannot log in: unexpected content type \"$contentType\" returned (text/xml expected).");
  91.             }
  92.             $xml = new SimpleXMLElement($res['body']);
  93.             if (! $sid = (string) $xml->challenge) {
  94.                 self::throwException('Cannot log in: no challenge found in response body.');
  95.             }
  96.             $this->_sessionId = trim($sid);
  97.            
  98.             ///////////////////
  99.             // 2. Generate hash for authentication
  100.             $hash = $this->_genLoginHash($sid, $this->_login, $this->_password);
  101.            
  102.             ///////////////////
  103.             // 3. Log in with calculated hash
  104.             try {
  105.                 $res = $this->_sendRawRequest('/login', 'post',
  106.                     array(
  107.                         'hash' => $hash,
  108.                         'login' => '',
  109.                         'method' => 'passwd',
  110.                         'password' => '',
  111.                         'zsid' => $sid,
  112.                     ),
  113.                     array(),
  114.                     'zsid=' . $sid
  115.                 );
  116.             }
  117.             catch (Exception $e) {
  118.                 self::throwException("Cannot log in: authentication request failed. ({$e->getMessage()})");
  119.             }
  120.            
  121.             if (200 != ($code = $res['info']['http_code'])) {
  122.                 self::throwException("Cannot log in: unexpected code HTTP $code returned while attempting to authenticate.");
  123.             }
  124.             $this->log('Login successful! Session ID: ' . $sid);
  125.         }
  126.     }
  127.    
  128.     /**
  129.      * Generates the authentication hash based on session ID,
  130.      * login and password.
  131.      *
  132.      * @param string $challenge
  133.      * @param string $login
  134.      * @param string $password
  135.      */
  136.     protected function _genLoginHash($challenge, $login, $password) {
  137.         return hash_hmac('sha256', hash('sha256', $login), $challenge)
  138.             . hash_hmac('sha256', hash('sha256', $password), $challenge);
  139.     }
  140.    
  141.     /**
  142.      * Helper for building raw cURL requests.
  143.      *
  144.      * @param string $path
  145.      * @param string $method
  146.      * @param array $data
  147.      * @return array
  148.      * @throws Exception if the request failed.
  149.      */
  150.     protected function _sendRawRequest($path = '/', $method = 'get', $data = array(), $headers = null, $cookie = '') {
  151.         curl_setopt($this->_curl, CURLOPT_URL, $this->_getUrl($path));
  152.         if ($cookie) {
  153.             curl_setopt($this->_curl, CURLOPT_COOKIE, $cookie);
  154.         }
  155.         curl_setopt($this->_curl, CURLOPT_TIMEOUT, self::REQUEST_TIMEOUT);
  156.         curl_setopt($this->_curl, CURLOPT_RETURNTRANSFER, true);
  157.        
  158.         if ($method == 'get') {
  159.             curl_setopt($this->_curl, CURLOPT_HTTPGET, true);
  160.         }
  161.         elseif ($method == 'post') {
  162.             curl_setopt($this->_curl, CURLOPT_POST, true);
  163.             $postfields = array();
  164.             foreach($data as $key => $value) {
  165.                 $postfields[] = $key . '=' . $value;
  166.             }
  167.             $postfields = implode(';', $postfields);
  168.             curl_setopt($this->_curl, CURLOPT_POSTFIELDS, $postfields);
  169.         }
  170.         if (is_array($headers)) {
  171.             curl_setopt($this->_curl, CURLOPT_HTTPHEADER, $headers);
  172.         }
  173.         else {
  174.             curl_setopt($this->_curl, CURLOPT_HTTPHEADER, array());
  175.         }
  176.        
  177.         curl_setopt($this->_curl, CURLOPT_VERBOSE, $this->debug ? true : false);
  178.        
  179.         $result = curl_exec($this->_curl);
  180.         if ($result === false) {
  181.             self::throwException('cURL error ' . curl_errno($this->_curl));
  182.         }
  183.        
  184.         return array(
  185.             'info'  => curl_getinfo($this->_curl),
  186.             'body'  => $result,
  187.         );
  188.     }
  189.    
  190.     /**
  191.      * Helper for building cURL requests after authentication with
  192.      * the Neufbox.
  193.      *
  194.      * @param string $path
  195.      * @param string $method
  196.      * @param array $data
  197.      * @return array
  198.      * @throws Exception if the request failed.
  199.      */
  200.     protected function _sendRequest($path = '/', $method = 'get', $data = array(), $headers = null) {
  201.         if (!$this->_sessionId) {
  202.             $this->log("No session, initializing...", self::LOG_NOTICE);
  203.             $this->_login();
  204.         }
  205.         $res = $this->_sendRawRequest($path, $method, $data, $headers, self::COOKIE_NAME . '=' . $this->_sessionId);
  206.        
  207.         // Redirect means that session is invalid
  208.         if ($res['info']['http_code'] == 302) {
  209.             $this->log("Session lost, attempting to renew...", self::LOG_NOTICE);
  210.            
  211.             // Force new login
  212.             $this->_login(true);
  213.            
  214.             // Then try the original request again
  215.             $res = $this->_sendRawRequest($path, $method, $data, $headers, self::COOKIE_NAME . '=' . $this->_sessionId);
  216.             if ($res['info']['http_code'] != 200) {
  217.                 self::throwException('Cannot reconnect to Neufbox. Aborting.');
  218.             }
  219.         }
  220.         return $res;
  221.     }
  222.    
  223.     /**
  224.      *
  225.      * @param string $html
  226.      * @param string $encoding
  227.      * @return DOMXPath
  228.      */
  229.     protected function _htmlToDOMXPath($html, $encoding = 'iso-8859-1') {
  230.         $dom = new DOMDocument('1.0', $encoding);
  231.         $dom->loadHTML($html);
  232.         $xpathDom = new DOMXPath($dom);
  233.        
  234.         return $xpathDom;
  235.     }
  236.    
  237.     public function login($login, $password) {
  238.         $this->_login = $login;
  239.         $this->_password = $password;
  240.         $this->_login(true);
  241.     }
  242.    
  243.     public function logout() {
  244.         $this->log('Logging out.', self::LOG_NOTICE);
  245.         $this->_sessionId = null;
  246.     }
  247.    
  248.     public function getHost() {
  249.         return $this->_host;
  250.     }
  251.    
  252.     protected function _getStatusFromNodeCss($html, $xpath) {
  253.         $dom = $this->_htmlToDOMXPath($html);
  254.         $entries = $dom->query($xpath);
  255.         if (null === $entries->item(0)) {
  256.             self::throwException('Cannot find node at XPath "' . $xpath . '".');
  257.         }
  258.         $entry = $entries->item(0);
  259.        
  260.         $status = null;
  261.         switch($entry->attributes->getNamedItem('class')->nodeValue) {
  262.             case 'enabled':
  263.                 $status = self::STATUS_CONNECTED;
  264.                 break;
  265.                
  266.             case 'disabled':
  267.                 $status = self::STATUS_NOT_CONNECTED;
  268.                 break;
  269.                
  270.             case 'unused':
  271.                 $status = self::STATUS_UNUSED;
  272.                 break;
  273.         }
  274.         return $status;
  275.     }
  276.    
  277.     /**
  278.      *
  279.      * @param string $html
  280.      * @param string $xpath XPath to the table holding data to be retrieved
  281.      */
  282.     protected function _getTableDataAsArray($html, $xpath) {
  283.         $dom = $this->_htmlToDOMXPath($html);
  284.         $entries = $dom->query($xpath);
  285.         if (null === $entries->item(0) || $entries->item(0)->nodeName != 'table') {
  286.             self::throwException('Cannot find <table> node at XPath "' . $xpath . '".');
  287.         }
  288.         $entry = $entries->item(0);
  289.        
  290.         $data = array();
  291.         foreach($entry->childNodes as $childNode) {
  292.             /** $childNode <tr> */
  293.             $label = '';
  294.             $value = '';
  295.             foreach($childNode->childNodes as $node) {
  296.                 if ($node->nodeName == 'th') {
  297.                     $label = self::_normalizeText($node->textContent);
  298.                 }
  299.                 if ($node->nodeName == 'td') {
  300.                     $value = self::_normalizeText($node->textContent);
  301.                 }
  302.             }
  303.             if ($label && $value) {
  304.                 $data[$label] = $value;
  305.             }
  306.         }
  307.         return $data;
  308.     }
  309.    
  310.     /**
  311.      *
  312.      * @param string $html
  313.      * @param string $xpath XPath to the table holding data to be retrieved
  314.      */
  315.     protected function _getTableDataAsArrayWithHeaders($html, $xpath) {
  316.         $dom = $this->_htmlToDOMXPath($html);
  317.         $entries = $dom->query($xpath);
  318.         if (null === $entries->item(0) || $entries->item(0)->nodeName != 'table') {
  319.             self::throwException('Cannot find <table> node at XPath "' . $xpath . '".');
  320.         }
  321.         $entry = $entries->item(0);
  322.        
  323.         $data = array();
  324.        
  325.         // Cols
  326.         $cols = array();
  327.         $nodeList = $dom->query($xpath . '/thead/tr/th');
  328.         foreach($nodeList as $node) {
  329.             if ($node->nodeName == 'th') {
  330.                 $cols[] = self::_normalizeText($node->textContent);
  331.             }
  332.         }
  333.        
  334.         // Rows
  335.         $rows = array();
  336.         $rowNodeList = $dom->query($xpath . '/tbody/tr');
  337.         $i = 0;
  338.         foreach($rowNodeList as $rowNode) {
  339.             $i++;
  340.             $j = 0;
  341.             foreach($rowNode->childNodes as $cellNode) {
  342.                 if ($cellNode->nodeName == 'td') {
  343.                     $colName = isset($cols[$j]) ? $cols[$j++] : "{Column $j}";
  344.                    
  345.                     // Normal text node
  346.                     if ($value = self::_normalizeText($cellNode->textContent)) {
  347.                         $data[$i][$colName] = $value;
  348.                     }
  349.                     else {
  350.                         foreach($cellNode->childNodes as $subCellNode) {
  351.                             // Image node: retrieve "alt" attribute as text value
  352.                             if ($subCellNode->nodeName == 'img') {
  353.                                 if ($value = $subCellNode->getAttribute('alt')) {
  354.                                     $data[$i][$colName] = self::_normalizeText($value);
  355.                                 }
  356.                             }
  357.                         }
  358.                     }
  359.                    
  360.                     // Fallback
  361.                     if (!isset($data[$i][$colName])) {
  362.                         $data[$i][$colName] = '';
  363.                     }
  364.                 }
  365.             }
  366.         }
  367.         return $data;
  368.     }
  369.    
  370.     /**
  371.      *
  372.      * @return int
  373.      */
  374.     public function getIpv4Status() {
  375.         $this->log("Retrieving IPv4 status...");
  376.         $res = $this->_sendRequest('/state');
  377.         return $this->_getStatusFromNodeCss($res['body'], '//td[@id="internet_status"]');
  378.     }
  379.    
  380.     /**
  381.      *
  382.      * @return int
  383.      */
  384.     public function getIpv6Status() {
  385.         $this->log("Retrieving IPv6 status...");
  386.         $res = $this->_sendRequest('/state');
  387.         return $this->_getStatusFromNodeCss($res['body'], '//td[@id="internet_status_v6"]');
  388.     }
  389.    
  390.     /**
  391.      *
  392.      * @return int
  393.      */
  394.     public function getPhoneStatus() {
  395.         $this->log("Retrieving phone status...");
  396.         $res = $this->_sendRequest('/state');
  397.         return $this->_getStatusFromNodeCss($res['body'], '//td[@id="voip_status"]');
  398.     }
  399.    
  400.     /**
  401.      *
  402.      * @return int
  403.      */
  404.     public function getWifiStatus() {
  405.         $this->log("Retrieving Wifi status...");
  406.         $res = $this->_sendRequest('/wifi');
  407.         return $this->_getStatusFromNodeCss($res['body'], '//table[@id="wifi_info"]/*/td[1]');
  408.     }
  409.    
  410.     /**
  411.      *
  412.      * @return int
  413.      */
  414.     public function getTelevisionStatus() {
  415.         $this->log("Retrieving TV status...");
  416.         $res = $this->_sendRequest('/state');
  417.         return $this->_getStatusFromNodeCss($res['body'], '//td[@id="tv_status"]');
  418.     }
  419.    
  420.     /**
  421.      *
  422.      * @return array
  423.      */
  424.     public function getModemInfo() {
  425.         $this->log("Retrieving modem info...");
  426.         $res = $this->_sendRequest('/state');
  427.         return $this->_getTableDataAsArray($res['body'], '//table[@id="modem_infos"]');
  428.     }
  429.    
  430.     /**
  431.      *
  432.      * @return array
  433.      */
  434.     public function getIpv4ConnectionInfo() {
  435.         $this->log("Retrieving IPv4 info...");
  436.         $res = $this->_sendRequest('/state/wan');
  437.         return $this->_getTableDataAsArray($res['body'], '//table[@id="wan_info"]');
  438.     }
  439.    
  440.     /**
  441.      *
  442.      * @return array
  443.      */
  444.     public function getIpv6ConnectionInfo() {
  445.         $this->log("Retrieving IPv6 info...");
  446.         $res = $this->_sendRequest('/state/wan');
  447.         return $this->_getTableDataAsArray($res['body'], '//table[@id="ipv6_info"]');
  448.     }
  449.    
  450.     /**
  451.      *
  452.      * @return array
  453.      */
  454.     public function getAdslInfo() {
  455.         $this->log("Retrieving ADSL info...");
  456.         $res = $this->_sendRequest('/state/wan');
  457.         return $this->_getTableDataAsArray($res['body'], '//table[@id="adsl_info"]');
  458.     }
  459.    
  460.     /**
  461.      *
  462.      * @return array
  463.      */
  464.     public function getPppInfo() {
  465.         $this->log("Retrieving PPP info...");
  466.         $res = $this->_sendRequest('/state/wan');
  467.         return $this->_getTableDataAsArray($res['body'], '//table[@id="ppp_info"]');
  468.     }
  469.    
  470.     /**
  471.      *
  472.      * @return array
  473.      */
  474.     public function getConnectedHosts() {
  475.         $this->log("Retrieving connected hosts list...");
  476.         $res = $this->_sendRequest('/network');
  477.         return $this->_getTableDataAsArrayWithHeaders($res['body'], '//table[@id="network_clients"]');
  478.     }
  479.    
  480.     /**
  481.      *
  482.      * @return array
  483.      */
  484.     public function getPortsInfo() {
  485.         $this->log("Retrieving ports info...");
  486.         $res = $this->_sendRequest('/network');
  487.         return $this->_getTableDataAsArray($res['body'], '//table[@id="network_status"]');
  488.     }
  489.    
  490.     /**
  491.      *
  492.      * @return array
  493.      */
  494.     public function getWifiInfo() {
  495.         $this->log("Retrieving Wifi info...");
  496.         $res = $this->_sendRequest('/wifi');
  497.         return $this->_getTableDataAsArray($res['body'], '//table[@id="wifi_info"]');
  498.     }
  499.    
  500.     /**
  501.      *
  502.      * @return array
  503.      */
  504.     public function getNatConfig() {
  505.         $this->log("Retrieving NAT configuration...");
  506.         $res = $this->_sendRequest('/network/nat');
  507.         $return = $this->_getTableDataAsArrayWithHeaders($res['body'], '//table[@id="nat_config"]');
  508.        
  509.         //Remove last line (used to add a new NAT rule from the GUI)
  510.         array_pop($return);
  511.        
  512.         // Remove the last two columns from rows (used to enable/disable and delete rules from the GUI)
  513.         foreach($return as &$row) {
  514.             array_pop($row);
  515.             array_pop($row);
  516.         }
  517.        
  518.         return $return;
  519.     }
  520.    
  521.     /**
  522.      *
  523.      * @return array
  524.      */
  525.     public function getPhoneCallHistory() {
  526.         $this->log("Retrieving phone call history...");
  527.         $res = $this->_sendRequest('/state/voip');
  528.         return $this->_getTableDataAsArrayWithHeaders($res['body'], '//table[@id="call_history_list"]');
  529.     }
  530.    
  531.     public function getFullReport() {
  532.         $report = array();
  533.         $myMethods = get_class_methods($this);
  534.         $excludedMethods = array('getFullReport');
  535.         sort($myMethods);
  536.        
  537.         foreach($myMethods as $methodName) {
  538.             if (substr($methodName, 0, 3) == 'get' && !in_array($methodName, $excludedMethods)) {
  539.                 $key = self::_uncamelize(substr($methodName, 3));
  540.                 $report[$key] = call_user_func(array($this, $methodName));
  541.             }
  542.         }
  543.    
  544.         return $report;
  545.     }
  546.    
  547.     public function reboot() {
  548.         $res = $this->_sendRequest('/reboot', 'post', array('submit' => ''));
  549.         if (200 != ($code = $res['info']['http_code'])) {
  550.             self::throwException("Reboot may have failed: unexpected code HTTP $code returned.");
  551.         }
  552.     }
  553.    
  554.     public function log($msg, $level = self::LOG_DEBUG) {
  555.         if ($level >= $this->logLevel) {
  556.             echo self::formatLog($msg, $level);
  557.         }
  558.     }
  559.    
  560.     public static function formatLog($msg, $level = self::LOG_DEBUG) {
  561.         switch($level) {
  562.             case self::LOG_NOTICE:
  563.                 $level = 'NOTICE';
  564.                 break;
  565.            
  566.             case self::LOG_WARNING:
  567.                 $level = 'WARN';
  568.                 break;
  569.            
  570.             case self::LOG_ERROR:
  571.                 $level = 'ERROR';
  572.                 break;
  573.                
  574.             default:
  575.                 $level = 'DEBUG';
  576.                 break;
  577.         }
  578.         return date('Y-m-d H:i:s') . " [$level] " . print_r($msg, true) . "\n";
  579.     }
  580.    
  581.     public function throwException($msg) {
  582.         $this->log($msg, self::LOG_ERROR);
  583.         throw new Exception($msg);
  584.     }
  585.    
  586.     protected static function _normalizeText($text) {
  587.         return trim(preg_replace('/\s+/', ' ', str_replace(array("\n", "\r\n"), '', $text)));
  588.     }
  589.    
  590.     protected function _uncamelize($string) {
  591.        return strtolower(preg_replace('/(.)([A-Z])/', '$1_$2', $string));
  592.     }
  593.    
  594.     public static function checkRequirements() {
  595.         $classes = array(
  596.             'DOMDocument',
  597.             'DOMXPath',
  598.             'SimpleXMLElement'
  599.         );
  600.         $functions = array(
  601.             'curl_init'
  602.         );
  603.        
  604.         if (-1 == version_compare(phpversion(), '5.1.2')) {
  605.             echo "WARNING: PHP 5.1.2 or above is required.\n";
  606.         }
  607.         foreach($classes as $class) {
  608.             if (!class_exists($class)) {
  609.                 throw new Exception("Missing required class/library: '$class'. Please check your PHP configuration.");
  610.             }
  611.         }
  612.         foreach($functions as $function) {
  613.             if (!function_exists($function)) {
  614.                 throw new Exception("Missing required function/library: '$function'. Please check your PHP configuration.");
  615.             }
  616.         }
  617.     }
  618. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement