remove and add some exceptions, add getUptime and fix derivedk alculation
[GitHub/Stricted/speedport-hybrid-php-api.git] / SpeedportHybrid.class.php
CommitLineData
a91317a6 1<?php
8162139f
S
2require_once('RebootException.class.php');
3require_once('RouterException.class.php');
4
1d934afe
S
5/**
6 * @author Jan Altensen (Stricted)
7 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
8 * @copyright 2015 Jan Altensen (Stricted)
9 */
21f877e4 10class SpeedportHybrid {
0dca2d61
S
11 /**
12 *
13 *
14 */
15 const VERSION = '1.0.2';
16
a91317a6
S
17 /**
18 * password-challenge
19 * @var string
20 */
21 private $challenge = '';
22
809e25bd
S
23 /**
24 * csrf_token
25 * @var string
26 */
27 private $token = '';
28
a91317a6
S
29 /**
30 * hashed password
31 * @var string
32 */
33 private $hash = '';
34
35 /**
36 * session cookie
37 * @var string
38 */
ac344b92 39 private $cookie = '';
a91317a6
S
40
41 /**
42 * router url
43 * @var string
44 */
b5a532a8 45 private $url = '';
a91317a6 46
c9e082da
S
47 /**
48 * derivedk cookie
49 * @var string
50 */
51 private $derivedk = '';
52
e58a96d1 53 public function __construct ($url = 'http://speedport.ip/') {
b5a532a8 54 $this->url = $url;
a91317a6
S
55 }
56
57 /**
58 * Requests the password-challenge from the router.
59 */
e58a96d1 60 private function getChallenge () {
b5a532a8 61 $path = 'data/Login.json';
a91317a6 62 $fields = array('csrf_token' => 'nulltoken', 'showpw' => 0, 'challengev' => 'null');
b5a532a8 63 $data = $this->sentRequest($path, $fields);
a91317a6 64 $data = json_decode($data['body'], true);
c2678616
S
65 $data = $this->getValues($data);
66
67 if (isset($data['challengev']) && !empty($data['challengev'])) {
ac344b92
S
68 return $data['challengev'];
69 }
70 else {
71 throw new RouterExeption('unable to get the challenge from the router');
a91317a6
S
72 }
73 }
74
75 /**
76 * login into the router with the given password
5e44cffa 77 *
a91317a6
S
78 * @param string $password
79 * @return boolean
80 */
81 public function login ($password) {
ac344b92 82 $this->challenge = $this->getChallenge();
e58a96d1 83
b5a532a8 84 $path = 'data/Login.json';
a91317a6
S
85 $this->hash = hash('sha256', $this->challenge.':'.$password);
86 $fields = array('csrf_token' => 'nulltoken', 'showpw' => 0, 'password' => $this->hash);
b5a532a8 87 $data = $this->sentRequest($path, $fields);
a91317a6 88 $json = json_decode($data['body'], true);
c2678616 89 $json = $this->getValues($json);
ac344b92 90
c2678616 91 if (isset($json['login']) && $json['login'] == 'success') {
ac344b92
S
92 $this->cookie = $this->getCookie($data);
93
94 $this->derivedk = $this->getDerviedk($password);
95
96 // get the csrf_token
97 $this->token = $this->getToken();
98
99 if ($this->checkLogin(false) === true) {
100 return true;
a91317a6
S
101 }
102 }
103
104 return false;
105 }
106
df1e1394
S
107 /**
108 * check if we are logged in
109 *
adcedd8b
S
110 * @param boolean $exception
111 * @return boolean
df1e1394 112 */
adcedd8b 113 public function checkLogin ($exception = true) {
ac344b92
S
114 // check if challenge or session is empty
115 if (empty($this->challenge) || empty($this->cookie)) {
adcedd8b
S
116 if ($exception === true) {
117 throw new RouterExeption('you musst be logged in to use this method');
118 }
119
0dca2d61
S
120 return false;
121 }
122
34701575 123 $path = 'data/SecureStatus.json';
df1e1394 124 $fields = array();
0dca2d61 125 $data = $this->sentRequest($path, $fields, true);
df1e1394
S
126
127 if (empty($data['body'])) {
8162139f 128 throw new RouterExeption('unable to get SecureStatus data');
df1e1394
S
129 }
130
131 $json = json_decode($data['body'], true);
132 $json = $this->getValues($json);
133
134 if ($json['loginstate'] != 1) {
adcedd8b
S
135 if ($exception === true) {
136 throw new RouterExeption('you musst be logged in to use this method');
137 }
138
df1e1394
S
139 return false;
140 }
141
142 return true;
143 }
144
a91317a6
S
145 /**
146 * logout
5e44cffa 147 *
58feafa2 148 * @return boolean
a91317a6
S
149 */
150 public function logout () {
adcedd8b 151 $this->checkLogin();
e58a96d1 152
b5a532a8 153 $path = 'data/Login.json';
df1e1394 154 $fields = array('csrf_token' => $this->token, 'logout' => 'byby');
58feafa2 155 $this->sentRequest($path, $fields, true);
adcedd8b 156 if ($this->checkLogin(false) === false) {
df1e1394
S
157 // reset challenge and session
158 $this->challenge = '';
ac344b92
S
159 $this->cookie = '';
160 $this->token = '';
161 $this->derivedk = '';
df1e1394 162
58feafa2 163 return true;
df1e1394 164 }
58feafa2
S
165
166 return false;
a91317a6
S
167 }
168
169 /**
170 * reboot the router
5e44cffa 171 *
58feafa2 172 * @return boolean
a91317a6
S
173 */
174 public function reboot () {
adcedd8b 175 $this->checkLogin();
e58a96d1 176
b5a532a8 177 $path = 'data/Reboot.json';
8162139f
S
178 $fields = array('csrf_token' => $this->token, 'reboot_device' => 'true');
179 $data = $this->sentEncryptedRequest($path, $fields, true);
14d4f286 180
a91317a6 181 $json = json_decode($data['body'], true);
8162139f 182 $json = $this->getValues($json);
a91317a6 183
8162139f
S
184 if ($json['status'] == 'ok') {
185 // throw an exception because router is unavailable for other tasks
186 // like $this->logout() or $this->checkLogin
187 throw new RebootException('Router Reboot');
188 }
58feafa2
S
189
190 return false;
a91317a6
S
191 }
192
87026df3
S
193 /**
194 * change dsl connection status
5e44cffa 195 *
87026df3 196 * @param string $status
58feafa2 197 * @return boolean
87026df3
S
198 */
199 public function changeConnectionStatus ($status) {
adcedd8b 200 $this->checkLogin();
e58a96d1 201
b5a532a8 202 $path = 'data/Connect.json';
87026df3
S
203
204 if ($status == 'online' || $status == 'offline') {
205 $fields = array('csrf_token' => 'nulltoken', 'showpw' => 0, 'password' => $this->hash, 'req_connect' => $status);
0dca2d61 206 $data = $this->sentRequest($path, $fields, true);
c2678616 207
8162139f 208 $json = json_decode($data['body'], true);
58feafa2 209 $json = $this->getValues($json);
c2678616 210
58feafa2
S
211 if ($json['status'] == 'ok') {
212 return true;
213 }
214 else {
215 return false;
216 }
87026df3
S
217 }
218 else {
8162139f 219 throw new RouterExeption();
87026df3
S
220 }
221 }
222
58feafa2
S
223 /**
224 * get uptime based on online (connection) time
225 *
226 * @return string
227 */
228 public function getUptime () {
229 // TODO: search for a better solution, calling Connect.json need some time
230 $data = $this->getData('Connect');
231 $data = $this->getValues($data);
232
233 return $data['days_online'];
234 }
235
a91317a6
S
236 /**
237 * return the given json as array
5e44cffa 238 *
a91317a6
S
239 * @param string $file
240 * @return array
241 */
242 public function getData ($file) {
ac344b92 243 if ($file != 'Status') $this->checkLogin();
e58a96d1 244
b5a532a8 245 $path = 'data/'.$file.'.json';
a91317a6 246 $fields = array();
0dca2d61 247 $data = $this->sentRequest($path, $fields, true);
a91317a6
S
248
249 if (empty($data['body'])) {
8162139f 250 throw new RouterExeption('unable to get '.$file.' data');
a91317a6
S
251 }
252
253 $json = json_decode($data['body'], true);
254
255 return $json;
256 }
257
5e44cffa
S
258 /**
259 * get the router syslog
260 *
261 * @return array
262 */
263 public function getSyslog() {
0dca2d61 264 return $this->exportData('0');
5e44cffa
S
265 }
266
a35e3e67
S
267 /**
268 * get the Missed Calls from router
269 *
270 * @return array
271 */
272 public function getMissedCalls() {
0dca2d61 273 return $this->exportData('1');
a35e3e67
S
274 }
275
15260eeb
S
276 /**
277 * get the Taken Calls from router
278 *
279 * @return array
280 */
281 public function getTakenCalls() {
0dca2d61 282 return $this->exportData('2');
15260eeb
S
283 }
284
abf641bb
S
285 /**
286 * get the Dialed Calls from router
287 *
288 * @return array
289 */
290 public function getDialedCalls() {
0dca2d61
S
291 return $this->exportData('3');
292 }
293
294 /**
295 * export data from router
296 *
297 * @return array
298 */
299 private function exportData ($type) {
adcedd8b 300 $this->checkLogin();
e58a96d1 301
b383ace1 302 $path = 'data/Syslog.json';
0dca2d61
S
303 $fields = array('exporttype' => $type);
304 $data = $this->sentRequest($path, $fields, true);
9bd25bb4 305
abf641bb 306 if (empty($data['body'])) {
8162139f 307 throw new RouterExeption('unable to get export data');
abf641bb
S
308 }
309
310 return explode("\n", $data['body']);
311 }
312
809e25bd
S
313 /**
314 * reconnect LTE
315 *
316 * @return array
317 */
c9e082da 318 public function reconnectLte () {
adcedd8b 319 $this->checkLogin();
e58a96d1 320
c9e082da 321 $path = 'data/modules.json';
809e25bd 322 $fields = array('csrf_token' => $this->token, 'lte_reconn' => '1');
8162139f 323 $data = $this->sentEncryptedRequest($path, $fields, true);
c9e082da
S
324 $json = json_decode($data['body'], true);
325
326 return $json;
327 }
809e25bd 328
7f4a51d2
S
329 /**
330 * reset the router to Factory Default
331 * not tested
332 *
333 * @return array
334 */
9bd25bb4 335 public function resetToFactoryDefault () {
adcedd8b 336 $this->checkLogin();
e58a96d1 337
9bd25bb4
S
338 $path = 'data/resetAllSetting.json';
339 $fields = array('csrf_token' => 'nulltoken', 'showpw' => 0, 'password' => $this->hash, 'reset_all' => 'true');
0dca2d61 340 $data = $this->sentRequest($path, $fields, true);
9bd25bb4
S
341 $json = json_decode($data['body'], true);
342
343 return $json;
344 }
7f4a51d2 345
9bd25bb4 346
4ae464d1
S
347 /**
348 * check if firmware is actual
349 *
350 * @return array
351 */
352 public function checkFirmware () {
adcedd8b 353 $this->checkLogin();
e58a96d1 354
4ae464d1
S
355 $path = 'data/checkfirmware.json';
356 $fields = array('checkfirmware' => 'true');
0dca2d61 357 $data = $this->sentRequest($path, $fields, true);
4ae464d1
S
358
359 if (empty($data['body'])) {
8162139f 360 throw new RouterExeption('unable to get checkfirmware data');
4ae464d1
S
361 }
362
363 $json = json_decode($data['body'], true);
364
365 return $json;
366 }
367
14d4f286
S
368 /**
369 * decrypt data from router
370 *
371 * @param string $data
372 * @return array
373 */
c2678616 374 private function decrypt ($data) {
14d4f286
S
375 require_once 'CryptLib/CryptLib.php';
376 $factory = new CryptLib\Cipher\Factory();
377 $aes = $factory->getBlockCipher('rijndael-128');
378
379 $iv = hex2bin(substr($this->challenge, 16, 16));
380 $adata = hex2bin(substr($this->challenge, 32, 16));
381 $dkey = hex2bin($this->derivedk);
382 $enc = hex2bin($data);
383
384 $aes->setKey($dkey);
385 $mode = $factory->getMode('ccm', $aes, $iv, [ 'adata' => $adata, 'lSize' => 7]);
386
387 $mode->decrypt($enc);
388
389 return $mode->finish();
390 }
391
392 /**
393 * decrypt data for the router
394 *
395 * @param array $data
396 * @return string
397 */
c2678616 398 private function encrypt ($data) {
14d4f286
S
399 require_once 'CryptLib/CryptLib.php';
400 $factory = new CryptLib\Cipher\Factory();
401 $aes = $factory->getBlockCipher('rijndael-128');
402
403 $iv = hex2bin(substr($this->challenge, 16, 16));
404 $adata = hex2bin(substr($this->challenge, 32, 16));
405 $dkey = hex2bin($this->derivedk);
406
407 $aes->setKey($dkey);
408 $mode = $factory->getMode('ccm', $aes, $iv, [ 'adata' => $adata, 'lSize' => 7]);
409 $mode->encrypt(http_build_query($data));
410
809e25bd 411 return bin2hex($mode->finish());
14d4f286
S
412 }
413
c2678616
S
414 /**
415 * get the values from array
416 *
417 * @param array $array
418 * @return array
419 */
420 private function getValues($array) {
421 $data = array();
422 foreach ($array as $item) {
58feafa2
S
423 if (is_array($item['varvalue'])) {
424 $data[$item['varid']] = $this->getValues($item['varvalue']);
425 }
426 else {
427 $data[$item['varid']] = $item['varvalue'];
428 }
c2678616
S
429 }
430
431 return $data;
432 }
433
8162139f
S
434 /**
435 * sends the encrypted request to router
436 *
437 * @param string $path
438 * @param mixed $fields
439 * @param string $cookie
440 * @return array
441 */
442 private function sentEncryptedRequest ($path, $fields, $cookie = false) {
443 $count = count($fields);
444 $fields = $this->encrypt($fields);
445 return $this->sentRequest($path, $fields, $cookie, $count);
446 }
447
a91317a6
S
448 /**
449 * sends the request to router
5e44cffa 450 *
b5a532a8 451 * @param string $path
809e25bd 452 * @param mixed $fields
a91317a6 453 * @param string $cookie
809e25bd 454 * @param integer $count
a91317a6
S
455 * @return array
456 */
0dca2d61 457 private function sentRequest ($path, $fields, $cookie = false, $count = 0) {
49abc701 458 $url = $this->url.$path.'?lang=en';
a91317a6
S
459 $ch = curl_init();
460 curl_setopt($ch, CURLOPT_URL, $url);
461
462 if (!empty($fields)) {
809e25bd
S
463 if (is_array($fields)) {
464 curl_setopt($ch, CURLOPT_POST, count($fields));
465 curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($fields));
466 }
467 else {
468 curl_setopt($ch, CURLOPT_POST, $count);
469 curl_setopt($ch, CURLOPT_POSTFIELDS, $fields);
470 }
a91317a6
S
471 }
472
0dca2d61 473 if ($cookie === true) {
ac344b92 474 curl_setopt($ch, CURLOPT_COOKIE, 'challengev='.$this->challenge.'; '.$this->cookie);
a91317a6
S
475 }
476
477 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
478 curl_setopt($ch, CURLOPT_HEADER, true);
479
a91317a6
S
480 if ($cookie) {
481
482 }
483
484 $result = curl_exec($ch);
485
486 $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
487 $header = substr($result, 0, $header_size);
488 $body = substr($result, $header_size);
489 curl_close($ch);
490
8162139f
S
491 // check if body is encrypted (hex instead of json)
492 if (ctype_xdigit($body)) {
493 $body = $this->decrypt($body);
494 }
495
a91317a6
S
496 // fix invalid json
497 $body = preg_replace("/(\r\n)|(\r)/", "\n", $body);
498 $body = preg_replace('/\'/i', '"', $body);
5e44cffa 499 $body = preg_replace("/\[\s+\]/i", '[ {} ]', $body);
dae16c50 500 $body = preg_replace("/},\s+]/", "}\n]", $body);
a91317a6
S
501
502 return array('header' => $this->parse_headers($header), 'body' => $body);
503 }
504
809e25bd
S
505 /**
506 * get the csrf_token
507 *
508 * @return string
509 */
510 private function getToken () {
adcedd8b 511 $this->checkLogin();
e58a96d1 512
49abc701 513 $path = 'html/content/overview/index.html';
809e25bd 514 $fields = array();
0dca2d61 515 $data = $this->sentRequest($path, $fields, true);
809e25bd
S
516
517 if (empty($data['body'])) {
8162139f 518 throw new RouterExeption('unable to get csrf_token');
809e25bd
S
519 }
520
521 $a = explode('csrf_token = "', $data['body']);
522 $a = explode('";', $a[1]);
523
524 if (isset($a[0]) && !empty($a[0])) {
525 return $a[0];
526 }
527 else {
8162139f 528 throw new RouterExeption('unable to get csrf_token');
809e25bd
S
529 }
530 }
531
ac344b92
S
532 /**
533 * calculate the derivedk
534 *
535 * @param string $password
536 * @return string
537 */
538 private function getDerviedk ($password) {
539 $derivedk = '';
540
541 // calculate derivedk
542 if (!function_exists('hash_pbkdf2')) {
543 require_once 'CryptLib/CryptLib.php';
544 $pbkdf2 = new CryptLib\Key\Derivation\PBKDF\PBKDF2(array('hash' => 'sha1'));
545 $derivedk = bin2hex($pbkdf2->derive(hash('sha256', $password), substr($this->challenge, 0, 16), 1000, 32));
58feafa2 546 $derivedk = substr($derivedk, 0, 32);
ac344b92
S
547 }
548 else {
549 $derivedk = hash_pbkdf2('sha1', hash('sha256', $password), substr($this->challenge, 0, 16), 1000, 32);
550 }
551
58feafa2
S
552 if (empty($derivedk)) {
553 throw new RouterException('unable to calculate derivedk');
554 }
555
ac344b92
S
556 return $derivedk;
557 }
558
559 /**
560 * get cookie from header data
561 *
562 * @param array $data
563 * @return string
564 */
565 private function getCookie ($data) {
58feafa2 566 $cookie = '';
ac344b92
S
567 if (isset($data['header']['Set-Cookie']) && !empty($data['header']['Set-Cookie'])) {
568 preg_match('/^.*(SessionID_R3=[a-z0-9]*).*/i', $data['header']['Set-Cookie'], $match);
569 if (isset($match[1]) && !empty($match[1])) {
58feafa2 570 $cookie = $match[1];
ac344b92
S
571 }
572 }
573
58feafa2
S
574 if (empty($cookie)) {
575 throw new RouterExeption('unable to get the session cookie from the router');
576 }
577
578 return $cookie;
ac344b92
S
579 }
580
a91317a6
S
581 /**
582 * parse the curl return header into an array
5e44cffa 583 *
a91317a6
S
584 * @param string $response
585 * @return array
586 */
587 private function parse_headers($response) {
588 $headers = array();
589 $header_text = substr($response, 0, strpos($response, "\r\n\r\n"));
590
7f4a51d2
S
591 $header_text = explode("\r\n", $header_text);
592 foreach ($header_text as $i => $line) {
a91317a6
S
593 if ($i === 0) {
594 $headers['http_code'] = $line;
595 }
596 else {
597 list ($key, $value) = explode(': ', $line);
598 $headers[$key] = $value;
599 }
600 }
601
602 return $headers;
603 }
7f4a51d2 604}