move some stuff into own functions and rename some variables
[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 *
a91317a6
S
148 * @return array
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');
0dca2d61 155 $data = $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
S
162
163 $json = json_decode($data['body'], true);
164
165 return $json;
166 }
167 else {
8162139f 168 throw new RouterExeption('logout failed');
df1e1394 169 }
a91317a6
S
170 }
171
172 /**
173 * reboot the router
5e44cffa 174 *
a91317a6
S
175 * @return array
176 */
177 public function reboot () {
adcedd8b 178 $this->checkLogin();
e58a96d1 179
b5a532a8 180 $path = 'data/Reboot.json';
8162139f
S
181 $fields = array('csrf_token' => $this->token, 'reboot_device' => 'true');
182 $data = $this->sentEncryptedRequest($path, $fields, true);
14d4f286 183
a91317a6 184 $json = json_decode($data['body'], true);
8162139f 185 $json = $this->getValues($json);
a91317a6 186
8162139f
S
187 if ($json['status'] == 'ok') {
188 // throw an exception because router is unavailable for other tasks
189 // like $this->logout() or $this->checkLogin
190 throw new RebootException('Router Reboot');
191 }
192 else {
193 throw new RouterException('unable to reboot');
194 }
a91317a6
S
195 }
196
87026df3
S
197 /**
198 * change dsl connection status
5e44cffa 199 *
87026df3
S
200 * @param string $status
201 */
202 public function changeConnectionStatus ($status) {
adcedd8b 203 $this->checkLogin();
e58a96d1 204
b5a532a8 205 $path = 'data/Connect.json';
87026df3
S
206
207 if ($status == 'online' || $status == 'offline') {
208 $fields = array('csrf_token' => 'nulltoken', 'showpw' => 0, 'password' => $this->hash, 'req_connect' => $status);
0dca2d61 209 $data = $this->sentRequest($path, $fields, true);
c2678616 210
8162139f 211 $json = json_decode($data['body'], true);
c2678616
S
212
213 return $json;
87026df3
S
214 }
215 else {
8162139f 216 throw new RouterExeption();
87026df3
S
217 }
218 }
219
a91317a6
S
220 /**
221 * return the given json as array
5e44cffa 222 *
a91317a6
S
223 * @param string $file
224 * @return array
225 */
226 public function getData ($file) {
ac344b92 227 if ($file != 'Status') $this->checkLogin();
e58a96d1 228
b5a532a8 229 $path = 'data/'.$file.'.json';
a91317a6 230 $fields = array();
0dca2d61 231 $data = $this->sentRequest($path, $fields, true);
a91317a6
S
232
233 if (empty($data['body'])) {
8162139f 234 throw new RouterExeption('unable to get '.$file.' data');
a91317a6
S
235 }
236
237 $json = json_decode($data['body'], true);
238
239 return $json;
240 }
241
5e44cffa
S
242 /**
243 * get the router syslog
244 *
245 * @return array
246 */
247 public function getSyslog() {
0dca2d61 248 return $this->exportData('0');
5e44cffa
S
249 }
250
a35e3e67
S
251 /**
252 * get the Missed Calls from router
253 *
254 * @return array
255 */
256 public function getMissedCalls() {
0dca2d61 257 return $this->exportData('1');
a35e3e67
S
258 }
259
15260eeb
S
260 /**
261 * get the Taken Calls from router
262 *
263 * @return array
264 */
265 public function getTakenCalls() {
0dca2d61 266 return $this->exportData('2');
15260eeb
S
267 }
268
abf641bb
S
269 /**
270 * get the Dialed Calls from router
271 *
272 * @return array
273 */
274 public function getDialedCalls() {
0dca2d61
S
275 return $this->exportData('3');
276 }
277
278 /**
279 * export data from router
280 *
281 * @return array
282 */
283 private function exportData ($type) {
adcedd8b 284 $this->checkLogin();
e58a96d1 285
b383ace1 286 $path = 'data/Syslog.json';
0dca2d61
S
287 $fields = array('exporttype' => $type);
288 $data = $this->sentRequest($path, $fields, true);
9bd25bb4 289
abf641bb 290 if (empty($data['body'])) {
8162139f 291 throw new RouterExeption('unable to get export data');
abf641bb
S
292 }
293
294 return explode("\n", $data['body']);
295 }
296
809e25bd
S
297 /**
298 * reconnect LTE
299 *
300 * @return array
301 */
c9e082da 302 public function reconnectLte () {
adcedd8b 303 $this->checkLogin();
e58a96d1 304
c9e082da 305 $path = 'data/modules.json';
809e25bd 306 $fields = array('csrf_token' => $this->token, 'lte_reconn' => '1');
8162139f 307 $data = $this->sentEncryptedRequest($path, $fields, true);
c9e082da
S
308 $json = json_decode($data['body'], true);
309
310 return $json;
311 }
809e25bd 312
7f4a51d2
S
313 /**
314 * reset the router to Factory Default
315 * not tested
316 *
317 * @return array
318 */
9bd25bb4 319 public function resetToFactoryDefault () {
adcedd8b 320 $this->checkLogin();
e58a96d1 321
9bd25bb4
S
322 $path = 'data/resetAllSetting.json';
323 $fields = array('csrf_token' => 'nulltoken', 'showpw' => 0, 'password' => $this->hash, 'reset_all' => 'true');
0dca2d61 324 $data = $this->sentRequest($path, $fields, true);
9bd25bb4
S
325 $json = json_decode($data['body'], true);
326
327 return $json;
328 }
7f4a51d2 329
9bd25bb4 330
4ae464d1
S
331 /**
332 * check if firmware is actual
333 *
334 * @return array
335 */
336 public function checkFirmware () {
adcedd8b 337 $this->checkLogin();
e58a96d1 338
4ae464d1
S
339 $path = 'data/checkfirmware.json';
340 $fields = array('checkfirmware' => 'true');
0dca2d61 341 $data = $this->sentRequest($path, $fields, true);
4ae464d1
S
342
343 if (empty($data['body'])) {
8162139f 344 throw new RouterExeption('unable to get checkfirmware data');
4ae464d1
S
345 }
346
347 $json = json_decode($data['body'], true);
348
349 return $json;
350 }
351
14d4f286
S
352 /**
353 * decrypt data from router
354 *
355 * @param string $data
356 * @return array
357 */
c2678616 358 private function decrypt ($data) {
14d4f286
S
359 require_once 'CryptLib/CryptLib.php';
360 $factory = new CryptLib\Cipher\Factory();
361 $aes = $factory->getBlockCipher('rijndael-128');
362
363 $iv = hex2bin(substr($this->challenge, 16, 16));
364 $adata = hex2bin(substr($this->challenge, 32, 16));
365 $dkey = hex2bin($this->derivedk);
366 $enc = hex2bin($data);
367
368 $aes->setKey($dkey);
369 $mode = $factory->getMode('ccm', $aes, $iv, [ 'adata' => $adata, 'lSize' => 7]);
370
371 $mode->decrypt($enc);
372
373 return $mode->finish();
374 }
375
376 /**
377 * decrypt data for the router
378 *
379 * @param array $data
380 * @return string
381 */
c2678616 382 private function encrypt ($data) {
14d4f286
S
383 require_once 'CryptLib/CryptLib.php';
384 $factory = new CryptLib\Cipher\Factory();
385 $aes = $factory->getBlockCipher('rijndael-128');
386
387 $iv = hex2bin(substr($this->challenge, 16, 16));
388 $adata = hex2bin(substr($this->challenge, 32, 16));
389 $dkey = hex2bin($this->derivedk);
390
391 $aes->setKey($dkey);
392 $mode = $factory->getMode('ccm', $aes, $iv, [ 'adata' => $adata, 'lSize' => 7]);
393 $mode->encrypt(http_build_query($data));
394
809e25bd 395 return bin2hex($mode->finish());
14d4f286
S
396 }
397
c2678616
S
398 /**
399 * get the values from array
400 *
401 * @param array $array
402 * @return array
403 */
404 private function getValues($array) {
405 $data = array();
406 foreach ($array as $item) {
407 $data[$item['varid']] = $item['varvalue'];
408 }
409
410 return $data;
411 }
412
8162139f
S
413 /**
414 * sends the encrypted request to router
415 *
416 * @param string $path
417 * @param mixed $fields
418 * @param string $cookie
419 * @return array
420 */
421 private function sentEncryptedRequest ($path, $fields, $cookie = false) {
422 $count = count($fields);
423 $fields = $this->encrypt($fields);
424 return $this->sentRequest($path, $fields, $cookie, $count);
425 }
426
a91317a6
S
427 /**
428 * sends the request to router
5e44cffa 429 *
b5a532a8 430 * @param string $path
809e25bd 431 * @param mixed $fields
a91317a6 432 * @param string $cookie
809e25bd 433 * @param integer $count
a91317a6
S
434 * @return array
435 */
0dca2d61 436 private function sentRequest ($path, $fields, $cookie = false, $count = 0) {
49abc701 437 $url = $this->url.$path.'?lang=en';
a91317a6
S
438 $ch = curl_init();
439 curl_setopt($ch, CURLOPT_URL, $url);
440
441 if (!empty($fields)) {
809e25bd
S
442 if (is_array($fields)) {
443 curl_setopt($ch, CURLOPT_POST, count($fields));
444 curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($fields));
445 }
446 else {
447 curl_setopt($ch, CURLOPT_POST, $count);
448 curl_setopt($ch, CURLOPT_POSTFIELDS, $fields);
449 }
a91317a6
S
450 }
451
0dca2d61 452 if ($cookie === true) {
ac344b92 453 curl_setopt($ch, CURLOPT_COOKIE, 'challengev='.$this->challenge.'; '.$this->cookie);
a91317a6
S
454 }
455
456 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
457 curl_setopt($ch, CURLOPT_HEADER, true);
458
a91317a6
S
459 if ($cookie) {
460
461 }
462
463 $result = curl_exec($ch);
464
465 $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
466 $header = substr($result, 0, $header_size);
467 $body = substr($result, $header_size);
468 curl_close($ch);
469
8162139f
S
470 // check if body is encrypted (hex instead of json)
471 if (ctype_xdigit($body)) {
472 $body = $this->decrypt($body);
473 }
474
a91317a6
S
475 // fix invalid json
476 $body = preg_replace("/(\r\n)|(\r)/", "\n", $body);
477 $body = preg_replace('/\'/i', '"', $body);
5e44cffa 478 $body = preg_replace("/\[\s+\]/i", '[ {} ]', $body);
dae16c50 479 $body = preg_replace("/},\s+]/", "}\n]", $body);
a91317a6
S
480
481 return array('header' => $this->parse_headers($header), 'body' => $body);
482 }
483
809e25bd
S
484 /**
485 * get the csrf_token
486 *
487 * @return string
488 */
489 private function getToken () {
adcedd8b 490 $this->checkLogin();
e58a96d1 491
49abc701 492 $path = 'html/content/overview/index.html';
809e25bd 493 $fields = array();
0dca2d61 494 $data = $this->sentRequest($path, $fields, true);
809e25bd
S
495
496 if (empty($data['body'])) {
8162139f 497 throw new RouterExeption('unable to get csrf_token');
809e25bd
S
498 }
499
500 $a = explode('csrf_token = "', $data['body']);
501 $a = explode('";', $a[1]);
502
503 if (isset($a[0]) && !empty($a[0])) {
504 return $a[0];
505 }
506 else {
8162139f 507 throw new RouterExeption('unable to get csrf_token');
809e25bd
S
508 }
509 }
510
ac344b92
S
511 /**
512 * calculate the derivedk
513 *
514 * @param string $password
515 * @return string
516 */
517 private function getDerviedk ($password) {
518 $derivedk = '';
519
520 // calculate derivedk
521 if (!function_exists('hash_pbkdf2')) {
522 require_once 'CryptLib/CryptLib.php';
523 $pbkdf2 = new CryptLib\Key\Derivation\PBKDF\PBKDF2(array('hash' => 'sha1'));
524 $derivedk = bin2hex($pbkdf2->derive(hash('sha256', $password), substr($this->challenge, 0, 16), 1000, 32));
525 $derivedk = substr($this->derivedk, 0, 32);
526 }
527 else {
528 $derivedk = hash_pbkdf2('sha1', hash('sha256', $password), substr($this->challenge, 0, 16), 1000, 32);
529 }
530
531 return $derivedk;
532 }
533
534 /**
535 * get cookie from header data
536 *
537 * @param array $data
538 * @return string
539 */
540 private function getCookie ($data) {
541 if (isset($data['header']['Set-Cookie']) && !empty($data['header']['Set-Cookie'])) {
542 preg_match('/^.*(SessionID_R3=[a-z0-9]*).*/i', $data['header']['Set-Cookie'], $match);
543 if (isset($match[1]) && !empty($match[1])) {
544 return $match[1];
545 }
546 }
547
548 throw new RouterExeption('unable to get the session cookie from the router');
549 }
550
a91317a6
S
551 /**
552 * parse the curl return header into an array
5e44cffa 553 *
a91317a6
S
554 * @param string $response
555 * @return array
556 */
557 private function parse_headers($response) {
558 $headers = array();
559 $header_text = substr($response, 0, strpos($response, "\r\n\r\n"));
560
7f4a51d2
S
561 $header_text = explode("\r\n", $header_text);
562 foreach ($header_text as $i => $line) {
a91317a6
S
563 if ($i === 0) {
564 $headers['http_code'] = $line;
565 }
566 else {
567 list ($key, $value) = explode(': ', $line);
568 $headers[$key] = $value;
569 }
570 }
571
572 return $headers;
573 }
7f4a51d2 574}