/fritzbox_api.class.php

https://gitlab.com/williwacker/fritzbox_api_php · PHP · 530 lines · 308 code · 50 blank · 172 comment · 60 complexity · 744b948645f0483b73ac7ffb21bca9d1 MD5 · raw file

  1. <?php
  2. /**
  3. * Fritz!Box API - A simple wrapper for automatted changes in the Fritz!Box Web-UI
  4. *
  5. * handles the new secured login/session system and implements a cURL wrapper
  6. * new in v0.2: Can handle remote config mode via https://example.dyndns.org
  7. * new in v0.3: New method doGetRequest handles GET-requests
  8. * new in v0.4: Added support for the new .lua forms like the WLAN guest access settings
  9. * new in v5.0: added support for the new .lua-loginpage in newest Fritz!OS firmwares and refactored the code
  10. *
  11. * @author Gregor Nathanael Meyer <Gregor [at] der-meyer.de>
  12. * @license http://creativecommons.org/licenses/by-sa/3.0/de/ Creative Commons cc-by-sa
  13. * @version 0.5.0b7 2013-01-02
  14. * @package Fritz!Box PHP tools
  15. */
  16. /* A simple usage example
  17. *
  18. * try
  19. * {
  20. * // load the fritzbox_api class
  21. * require_once('fritzbox_api.class.php');
  22. * $fritz = new fritzbox_api();
  23. * // init the output message
  24. * $message = date('Y-m-d H:i') . ' ';
  25. *
  26. * // update the setting
  27. * $formfields = array(
  28. * 'telcfg:command/Dial' => '**610',
  29. * );
  30. * $fritz->doPostForm($formfields);
  31. * $message .= 'Phone ' . $dial . ' ringed.';
  32. * }
  33. * catch (Exception $e)
  34. * {
  35. * $message .= $e->getMessage();
  36. * }
  37. *
  38. * // log the result
  39. * $fritz->logMessage($message);
  40. * $fritz = null; // destroy the object to log out
  41. */
  42. /**
  43. * the main Fritz!Box API class
  44. *
  45. */
  46. class fritzbox_api {
  47. /**
  48. * @var object config object
  49. */
  50. public $config = array();
  51. /**
  52. * @var string the session ID, set by method initSID() after login
  53. */
  54. protected $sid = '0000000000000000';
  55. /**
  56. * the constructor, initializes the object and calls the login method
  57. *
  58. * @access public
  59. */
  60. public function __construct($config_version = 'standard')
  61. {
  62. // init the config object
  63. $this->config = new fritzbox_api_config();
  64. if ( $config_version != 'standard' )
  65. {
  66. // try autoloading the $config_version_config
  67. if ( file_exists(__DIR__ . '/fritzbox_user_' . $config_version . '.conf.php') && is_readable(__DIR__ . '/fritzbox_user_' . $config_version . '.conf.php') )
  68. {
  69. require_once(__DIR__ . '/fritzbox_user_' . $config_version . '.conf.php');
  70. }
  71. else
  72. {
  73. $this->error('Could not load ' . __DIR__ . '/fritzbox_user_' . $config_version . '.conf.php');
  74. }
  75. }
  76. else
  77. {
  78. // try autoloading the config
  79. if ( file_exists(__DIR__ . '/fritzbox_user.conf.php') && is_readable(__DIR__ . '/fritzbox_user.conf.php') )
  80. {
  81. require_once(__DIR__ . '/fritzbox_user.conf.php');
  82. }
  83. }
  84. // make some config consistency checks
  85. if ( $this->config->getItem('enable_remote_config') === true )
  86. {
  87. if ( !$this->config->getItem('remote_config_user') || !$this->config->getItem('remote_config_password') )
  88. {
  89. $this->error('ERROR: Remote config mode enabled, but no username or no password provided');
  90. }
  91. $this->config->setItem('fritzbox_url', 'https://' . $this->config->getItem('fritzbox_ip'));
  92. }
  93. else
  94. {
  95. $this->config->setItem('fritzbox_url', 'http://' . $this->config->getItem('fritzbox_ip'));
  96. $this->config->setItem('old_remote_config_user', null);
  97. $this->config->setItem('old_remote_config_password', null);
  98. }
  99. $this->sid = $this->initSID();
  100. }
  101. /**
  102. * the destructor just calls the logout method
  103. *
  104. * @access public
  105. */
  106. public function __destruct()
  107. {
  108. $this->logout();
  109. }
  110. /**
  111. * do a POST request on the box
  112. * the main cURL wrapper handles the command
  113. *
  114. * @param array $formfields an associative array with the POST fields to pass
  115. * @return string the raw HTML code returned by the Fritz!Box
  116. */
  117. public function doPostForm($formfields = array())
  118. {
  119. $ch = curl_init();
  120. if ( isset($formfields['getpage']) && strpos($formfields['getpage'], '.lua') > 0 )
  121. {
  122. curl_setopt($ch, CURLOPT_URL, $this->config->getItem('fritzbox_url') . $formfields['getpage'] . '?sid=' . $this->sid);
  123. unset($formfields['getpage']);
  124. }
  125. else
  126. {
  127. // add the sid, if it is already set
  128. if ($this->sid != '0000000000000000')
  129. {
  130. $formfields['sid'] = $this->sid;
  131. }
  132. curl_setopt($ch, CURLOPT_URL, $this->config->getItem('fritzbox_url') . '/cgi-bin/webcm');
  133. }
  134. curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  135. curl_setopt($ch, CURLOPT_POST, 1);
  136. if ( $this->config->getItem('enable_remote_config') )
  137. {
  138. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  139. // support for pre FRITZ!OS 5.50 remote config
  140. if ( !$this->config->getItem('use_lua_login_method') )
  141. {
  142. curl_setopt($ch, CURLOPT_USERPWD, $this->config->getItem('remote_config_user') . ':' . $this->config->getItem('remote_config_password'));
  143. }
  144. }
  145. curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($formfields));
  146. $output = curl_exec($ch);
  147. curl_close($ch);
  148. return $output;
  149. }
  150. public function doPostFile($formfields = array(), $filefields = array())
  151. {
  152. $ch = curl_init();
  153. // add the sid, if it is already set
  154. if ($this->sid != '0000000000000000')
  155. {
  156. $formfields = array_merge(array('sid' => $this->sid), $formfields);
  157. }
  158. curl_setopt($ch, CURLOPT_URL, $this->config->getItem('fritzbox_url') . '/cgi-bin/firmwarecfg');
  159. curl_setopt($ch, CURLOPT_POST, 1);
  160. if ( $this->config->getItem('enable_remote_config') )
  161. {
  162. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  163. // support for pre FRITZ!OS 5.50 remote config
  164. if ( !$this->config->getItem('use_lua_login_method') )
  165. {
  166. curl_setopt($ch, CURLOPT_USERPWD, $this->config->getItem('remote_config_user') . ':' . $this->config->getItem('remote_config_password'));
  167. }
  168. }
  169. // enable for debugging:
  170. // curl_setopt($ch, CURLOPT_VERBOSE, TRUE);
  171. curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  172. // if filefields not specified ('@/path/to/file.xml;type=text/xml' works fine)
  173. if(empty( $filefields )) {
  174. curl_setopt($ch, CURLOPT_POSTFIELDS, $formfields); // http_build_query
  175. }
  176. // post calculated raw data
  177. else {
  178. $header = $this->_create_custom_file_post_header($formfields, $filefields);
  179. curl_setopt($ch, CURLOPT_HTTPHEADER , array(
  180. 'Content-Type: multipart/form-data; boundary=' . $header['delimiter'], 'Content-Length: ' . strlen($header['data']) )
  181. );
  182. curl_setopt($ch, CURLOPT_POSTFIELDS, $header['data']);
  183. }
  184. $output = curl_exec($ch);
  185. // curl error
  186. if(curl_errno($ch)) {
  187. $this->error('ERROR: '.curl_error($ch)." (".curl_errno($ch).")");
  188. }
  189. // finger out an error message, if given
  190. preg_match('@<p class="ErrorMsg">(.*?)</p>@is', $output, $matches);
  191. if (isset($matches[1]))
  192. {
  193. $this->error('ERROR: '.str_replace('&nbsp;', ' ', $matches[1]));
  194. }
  195. curl_close($ch);
  196. return $output;
  197. }
  198. private function _create_custom_file_post_header($postFields, $fileFields) {
  199. // form field separator
  200. $delimiter = '-------------' . uniqid();
  201. /*
  202. // file upload fields: name => array(type=>'mime/type',content=>'raw data')
  203. $fileFields = array(
  204. 'file1' => array(
  205. 'type' => 'text/xml',
  206. 'content' => '...your raw file content goes here...',
  207. 'filename' = 'filename.xml'
  208. ),
  209. );
  210. // all other fields (not file upload): name => value
  211. $postFields = array(
  212. 'otherformfield' => 'content of otherformfield is this text',
  213. );
  214. */
  215. $data = '';
  216. // populate normal fields first (simpler)
  217. foreach ($postFields as $name => $content) {
  218. $data .= "--" . $delimiter . "\r\n";
  219. $data .= 'Content-Disposition: form-data; name="' . urlencode($name) . '"';
  220. $data .= "\r\n\r\n";
  221. $data .= $content;
  222. $data .= "\r\n";
  223. }
  224. // populate file fields
  225. foreach ($fileFields as $name => $file) {
  226. $data .= "--" . $delimiter . "\r\n";
  227. // "filename" attribute is not essential; server-side scripts may use it
  228. $data .= 'Content-Disposition: form-data; name="' . urlencode($name) . '";' .
  229. ' filename="' . $file['filename'] . '"' . "\r\n";
  230. //$data .= 'Content-Transfer-Encoding: binary'."\r\n";
  231. // this is, again, informative only; good practice to include though
  232. $data .= 'Content-Type: ' . $file['type'] . "\r\n";
  233. // this endline must be here to indicate end of headers
  234. $data .= "\r\n";
  235. // the file itself (note: there's no encoding of any kind)
  236. $data .= $file['content'] . "\r\n";
  237. }
  238. // last delimiter
  239. $data .= "--" . $delimiter . "--\r\n";
  240. return array('delimiter' => $delimiter, 'data' => $data);
  241. }
  242. /**
  243. * do a GET request on the box
  244. * the main cURL wrapper handles the command
  245. *
  246. * @param array $params an associative array with the GET params to pass
  247. * @return string the raw HTML code returned by the Fritz!Box
  248. */
  249. public function doGetRequest($params = array())
  250. {
  251. // add the sid, if it is already set
  252. if ($this->sid != '0000000000000000')
  253. {
  254. $params['sid'] = $this->sid;
  255. }
  256. $ch = curl_init();
  257. if ( strpos($params['getpage'], '.lua') > 0 )
  258. {
  259. $getpage = $params['getpage'] . '?';
  260. unset($params['getpage']);
  261. }
  262. else
  263. {
  264. $getpage = '/cgi-bin/webcm?';
  265. }
  266. curl_setopt($ch, CURLOPT_URL, $this->config->getItem('fritzbox_url') . $getpage . http_build_query($params));
  267. curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  268. curl_setopt($ch, CURLOPT_HTTPGET, 1);
  269. if ( $this->config->getItem('enable_remote_config') )
  270. {
  271. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  272. // support for pre FRITZ!OS 5.50 remote config
  273. if ( !$this->config->getItem('use_lua_login_method') )
  274. {
  275. curl_setopt($ch, CURLOPT_USERPWD, $this->config->getItem('remote_config_user') . ':' . $this->config->getItem('remote_config_password'));
  276. }
  277. }
  278. $output = curl_exec($ch);
  279. curl_close($ch);
  280. return $output;
  281. }
  282. /**
  283. * the login method, handles the secured login-process
  284. * newer firmwares (xx.04.74 and newer) need a challenge-response mechanism to prevent Cross-Site Request Forgery attacks
  285. * see http://www.avm.de/de/Extern/Technical_Note_Session_ID.pdf for details
  286. *
  287. * @return string a valid SID, if the login was successful, otherwise throws an Exception with an error message
  288. */
  289. protected function initSID()
  290. {
  291. // determine, wich login type we have to use
  292. if ( $this->config->getItem('use_lua_login_method') == true )
  293. {
  294. $loginpage = '/login_sid.lua';
  295. }
  296. else
  297. {
  298. $loginpage = '../html/login_sid.xml';
  299. }
  300. // read the current status
  301. $session_status_simplexml = simplexml_load_string($this->doGetRequest(array('getpage' => $loginpage)));
  302. if ( !is_object($session_status_simplexml) || get_class($session_status_simplexml) != 'SimpleXMLElement' )
  303. {
  304. $this->error('Response of initialization call ' . $loginpage . ' in ' . __FUNCTION__ . ' was not xml-formatted.');
  305. }
  306. // perhaps we already have a SID (i.e. when no password is set)
  307. if ( $session_status_simplexml->SID != '0000000000000000' )
  308. {
  309. return $session_status_simplexml->SID;
  310. }
  311. // we have to login and get a new SID
  312. else
  313. {
  314. // the challenge-response magic, pay attention to the mb_convert_encoding()
  315. $challenge = $session_status_simplexml->Challenge;
  316. // do the login
  317. $formfields = array(
  318. 'getpage' => $loginpage,
  319. );
  320. if ( $this->config->getItem('use_lua_login_method') )
  321. {
  322. if ( $this->config->getItem('enable_remote_config') )
  323. {
  324. $formfields['username'] = $this->config->getItem('remote_config_user');
  325. $response = $challenge . '-' . md5(mb_convert_encoding($challenge . '-' . $this->config->getItem('remote_config_password'), "UCS-2LE", "UTF-8"));
  326. }
  327. else
  328. {
  329. if ( $this->config->getItem('username') )
  330. {
  331. $formfields['username'] = $this->config->getItem('username');
  332. }
  333. // $response = $challenge . '-' . md5(mb_convert_encoding($challenge . '-' . $this->config->getItem('password'), "UCS-2LE", "UTF-8"));
  334. $response = $challenge . '-' . md5(mb_convert_encoding($challenge . '-' . trim(file_get_contents($this->config->getItem('password_file'))), "UCS-2LE", "UTF-8"));
  335. }
  336. $formfields['response'] = $response;
  337. }
  338. else
  339. {
  340. // $response = $challenge . '-' . md5(mb_convert_encoding($challenge . '-' . $this->config->getItem('password'), "UCS-2LE", "UTF-8"));
  341. $response = $challenge . '-' . md5(mb_convert_encoding($challenge . '-' . trim(file_get_contents($this->config->getItem('password_file'))), "UCS-2LE", "UTF-8"));
  342. $formfields['login:command/response'] = $response;
  343. }
  344. $output = $this->doPostForm($formfields);
  345. // finger out the SID from the response
  346. $session_status_simplexml = simplexml_load_string($output);
  347. if ( !is_object($session_status_simplexml) || get_class($session_status_simplexml) != 'SimpleXMLElement' )
  348. {
  349. $this->error('Response of login call to ' . $loginpage . ' in ' . __FUNCTION__ . ' was not xml-formatted.');
  350. }
  351. if ( $session_status_simplexml->SID != '0000000000000000' )
  352. {
  353. return (string)$session_status_simplexml->SID;
  354. }
  355. else
  356. {
  357. $this->error('ERROR: Login failed with an unknown response.');
  358. }
  359. }
  360. }
  361. /**
  362. * the logout method just sends a logout command to the Fritz!Box
  363. *
  364. */
  365. protected function logout()
  366. {
  367. if ( $this->config->getItem('use_lua_login_method') == true )
  368. {
  369. $this->doGetRequest(array('getpage' => '/home/home.lua', 'logout' => '1'));
  370. }
  371. else
  372. {
  373. $formfields = array(
  374. 'getpage' => '../html/de/menus/menu2.html',
  375. 'security:command/logout' => 'logout',
  376. );
  377. $this->doPostForm($formfields);
  378. }
  379. }
  380. /**
  381. * the error method just throws an Exception
  382. *
  383. * @param string $message an error message for the Exception
  384. */
  385. public function error($message = null)
  386. {
  387. throw new Exception($message);
  388. }
  389. /**
  390. * a getter for the session ID
  391. *
  392. * @return string $this->sid
  393. */
  394. public function getSID()
  395. {
  396. return $this->sid;
  397. }
  398. /**
  399. * log a message
  400. *
  401. * @param $message string the message to log
  402. */
  403. public function logMessage($message)
  404. {
  405. if ( $this->config->getItem('newline') == false )
  406. {
  407. $this->config->setItem('newline', (PHP_OS == 'WINNT') ? "\r\n" : "\n");
  408. }
  409. if ( $this->config->getItem('logging') == 'console' )
  410. {
  411. echo $message;
  412. }
  413. else if ( $this->config->getItem('logging') == 'silent' || $this->config->getItem('logging') == false )
  414. {
  415. // do nothing
  416. }
  417. else
  418. {
  419. if ( is_writable($this->config->getItem('logging')) || is_writable(dirname($this->config->getItem('logging'))) )
  420. {
  421. file_put_contents($this->config->getItem('logging'), $message . $this->config->getItem('newline'), FILE_APPEND);
  422. }
  423. else
  424. {
  425. echo('Error: Cannot log to non-writeable file or dir: ' . $this->config->getItem('logging'));
  426. }
  427. }
  428. }
  429. }
  430. class fritzbox_api_config {
  431. protected $config = array();
  432. public function __construct()
  433. {
  434. # use the new .lua login method in current (end 2012) labor and newer firmwares (Fritz!OS 5.50 and up)
  435. $this->setItem('use_lua_login_method', true);
  436. # set to your Fritz!Box IP address or DNS name (defaults to fritz.box), for remote config mode, use the dyndns-name like example.dyndns.org
  437. $this->setItem('fritzbox_ip', 'fritz.box');
  438. # if needed, enable remote config here
  439. #$this->setItem('enable_remote_config', true);
  440. #$this->setItem('remote_config_user', 'test');
  441. #$this->setItem('remote_config_password', 'test123');
  442. # set to your Fritz!Box username, if login with username is enabled (will be ignored, when remote config is enabled)
  443. $this->setItem('username', false);
  444. # set to your Fritz!Box password (defaults to no password)
  445. $this->setItem('password', false);
  446. # set the logging mechanism (defaults to console logging)
  447. $this->setItem('logging', 'console'); // output to the console
  448. #$this->setItem('logging', 'silent'); // do not output anything, be careful with this logging mode
  449. #$this->setItem('logging', 'tam.log'); // the path to a writeable logfile
  450. # the newline character for the logfile (does not need to be changed in most cases)
  451. $this->setItem('newline', (PHP_OS == 'WINNT') ? "\r\n" : "\n");
  452. }
  453. /* gets an item from the config
  454. *
  455. * @param $item string the item to get
  456. * @return mixed the value of the item
  457. */
  458. public function getItem($item = 'all')
  459. {
  460. if ( $item == 'all' )
  461. {
  462. return $this->config;
  463. }
  464. elseif ( isset($this->config[$item]) )
  465. {
  466. return $this->config[$item];
  467. }
  468. return false;
  469. }
  470. /* sets an item into the config
  471. *
  472. * @param $item string the item to set
  473. * @param $value mixed the value to store into the item
  474. */
  475. public function setItem($item, $value)
  476. {
  477. $this->config[$item] = $value;
  478. }
  479. }