1bcc4fbf11cc1f3c04b368b4994549939a64ef82
[GitHub/Stricted/speedport-hybrid-php-api.git] / SpeedportHybrid.class.php
1 <?php
2 require_once('RebootException.class.php');
3 require_once('RouterException.class.php');
4
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 */
10 class SpeedportHybrid {
11 /**
12 *
13 *
14 */
15 const VERSION = '1.0.2';
16
17 /**
18 * password-challenge
19 * @var string
20 */
21 private $challenge = '';
22
23 /**
24 * csrf_token
25 * @var string
26 */
27 private $token = '';
28
29 /**
30 * hashed password
31 * @var string
32 */
33 private $hash = '';
34
35 /**
36 * session cookie
37 * @var string
38 */
39 private $cookie = '';
40
41 /**
42 * router url
43 * @var string
44 */
45 private $url = '';
46
47 /**
48 * derivedk cookie
49 * @var string
50 */
51 private $derivedk = '';
52
53 public function __construct ($url = 'http://speedport.ip/') {
54 $this->url = $url;
55 }
56
57 /**
58 * Requests the password-challenge from the router.
59 */
60 private function getChallenge () {
61 $path = 'data/Login.json';
62 $fields = array('csrf_token' => 'nulltoken', 'showpw' => 0, 'challengev' => 'null');
63 $data = $this->sentRequest($path, $fields);
64 $data = json_decode($data['body'], true);
65 $data = $this->getValues($data);
66
67 if (isset($data['challengev']) && !empty($data['challengev'])) {
68 return $data['challengev'];
69 }
70 else {
71 throw new RouterExeption('unable to get the challenge from the router');
72 }
73 }
74
75 /**
76 * login into the router with the given password
77 *
78 * @param string $password
79 * @return boolean
80 */
81 public function login ($password) {
82 $this->challenge = $this->getChallenge();
83
84 $path = 'data/Login.json';
85 $this->hash = hash('sha256', $this->challenge.':'.$password);
86 $fields = array('csrf_token' => 'nulltoken', 'showpw' => 0, 'password' => $this->hash);
87 $data = $this->sentRequest($path, $fields);
88 $json = json_decode($data['body'], true);
89 $json = $this->getValues($json);
90
91 if (isset($json['login']) && $json['login'] == 'success') {
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;
101 }
102 }
103
104 return false;
105 }
106
107 /**
108 * check if we are logged in
109 *
110 * @param boolean $exception
111 * @return boolean
112 */
113 public function checkLogin ($exception = true) {
114 // check if challenge or session is empty
115 if (empty($this->challenge) || empty($this->cookie)) {
116 if ($exception === true) {
117 throw new RouterExeption('you musst be logged in to use this method');
118 }
119
120 return false;
121 }
122
123 $path = 'data/SecureStatus.json';
124 $fields = array();
125 $data = $this->sentRequest($path, $fields, true);
126
127 if (empty($data['body'])) {
128 throw new RouterExeption('unable to get SecureStatus data');
129 }
130
131 $json = json_decode($data['body'], true);
132 $json = $this->getValues($json);
133
134 if ($json['loginstate'] != 1) {
135 if ($exception === true) {
136 throw new RouterExeption('you musst be logged in to use this method');
137 }
138
139 return false;
140 }
141
142 return true;
143 }
144
145 /**
146 * logout
147 *
148 * @return boolean
149 */
150 public function logout () {
151 $this->checkLogin();
152
153 $path = 'data/Login.json';
154 $fields = array('csrf_token' => $this->token, 'logout' => 'byby');
155 $this->sentRequest($path, $fields, true);
156 if ($this->checkLogin(false) === false) {
157 // reset challenge and session
158 $this->challenge = '';
159 $this->cookie = '';
160 $this->token = '';
161 $this->derivedk = '';
162
163 return true;
164 }
165
166 return false;
167 }
168
169 /**
170 * reboot the router
171 *
172 * @return boolean
173 */
174 public function reboot () {
175 $this->checkLogin();
176
177 $path = 'data/Reboot.json';
178 $fields = array('csrf_token' => $this->token, 'reboot_device' => 'true');
179 $data = $this->sentEncryptedRequest($path, $fields, true);
180
181 $json = json_decode($data['body'], true);
182 $json = $this->getValues($json);
183
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 }
189
190 return false;
191 }
192
193 /**
194 * change dsl connection status
195 *
196 * @param string $status
197 * @return boolean
198 */
199 public function changeConnectionStatus ($status) {
200 $this->checkLogin();
201
202 $path = 'data/Connect.json';
203
204 if ($status == 'online' || $status == 'offline') {
205 $fields = array('csrf_token' => 'nulltoken', 'showpw' => 0, 'password' => $this->hash, 'req_connect' => $status);
206 $data = $this->sentRequest($path, $fields, true);
207
208 $json = json_decode($data['body'], true);
209 $json = $this->getValues($json);
210
211 if ($json['status'] == 'ok') {
212 return true;
213 }
214 else {
215 return false;
216 }
217 }
218 else {
219 throw new RouterExeption();
220 }
221 }
222
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
236 /**
237 * return the given json as array
238 *
239 * @param string $file
240 * @return array
241 */
242 public function getData ($file) {
243 if ($file != 'Status') $this->checkLogin();
244
245 $path = 'data/'.$file.'.json';
246 $fields = array();
247 $data = $this->sentRequest($path, $fields, true);
248
249 if (empty($data['body'])) {
250 throw new RouterExeption('unable to get '.$file.' data');
251 }
252
253 $json = json_decode($data['body'], true);
254
255 return $json;
256 }
257
258 /**
259 * get the router syslog
260 *
261 * @return array
262 */
263 public function getSyslog() {
264 return $this->exportData('0');
265 }
266
267 /**
268 * get the Missed Calls from router
269 *
270 * @return array
271 */
272 public function getMissedCalls() {
273 return $this->exportData('1');
274 }
275
276 /**
277 * get the Taken Calls from router
278 *
279 * @return array
280 */
281 public function getTakenCalls() {
282 return $this->exportData('2');
283 }
284
285 /**
286 * get the Dialed Calls from router
287 *
288 * @return array
289 */
290 public function getDialedCalls() {
291 return $this->exportData('3');
292 }
293
294 /**
295 * export data from router
296 *
297 * @return array
298 */
299 private function exportData ($type) {
300 $this->checkLogin();
301
302 $path = 'data/Syslog.json';
303 $fields = array('exporttype' => $type);
304 $data = $this->sentRequest($path, $fields, true);
305
306 if (empty($data['body'])) {
307 throw new RouterExeption('unable to get export data');
308 }
309
310 return explode("\n", $data['body']);
311 }
312
313 /**
314 * reconnect LTE
315 *
316 * @return array
317 */
318 public function reconnectLte () {
319 $this->checkLogin();
320
321 $path = 'data/modules.json';
322 $fields = array('csrf_token' => $this->token, 'lte_reconn' => '1');
323 $data = $this->sentEncryptedRequest($path, $fields, true);
324 $json = json_decode($data['body'], true);
325
326 return $json;
327 }
328
329 /**
330 * reset the router to Factory Default
331 * not tested
332 *
333 * @return array
334 */
335 public function resetToFactoryDefault () {
336 $this->checkLogin();
337
338 $path = 'data/resetAllSetting.json';
339 $fields = array('csrf_token' => 'nulltoken', 'showpw' => 0, 'password' => $this->hash, 'reset_all' => 'true');
340 $data = $this->sentRequest($path, $fields, true);
341 $json = json_decode($data['body'], true);
342
343 return $json;
344 }
345
346
347 /**
348 * check if firmware is actual
349 *
350 * @return array
351 */
352 public function checkFirmware () {
353 $this->checkLogin();
354
355 $path = 'data/checkfirmware.json';
356 $fields = array('checkfirmware' => 'true');
357 $data = $this->sentRequest($path, $fields, true);
358
359 if (empty($data['body'])) {
360 throw new RouterExeption('unable to get checkfirmware data');
361 }
362
363 $json = json_decode($data['body'], true);
364
365 return $json;
366 }
367
368 /**
369 * decrypt data from router
370 *
371 * @param string $data
372 * @return array
373 */
374 private function decrypt ($data) {
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 */
398 private function encrypt ($data) {
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
411 return bin2hex($mode->finish());
412 }
413
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) {
423 if (is_array($item['varvalue'])) {
424 $data[$item['varid']] = $this->getValues($item['varvalue']);
425 }
426 else {
427 $data[$item['varid']] = $item['varvalue'];
428 }
429 }
430
431 return $data;
432 }
433
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
448 /**
449 * sends the request to router
450 *
451 * @param string $path
452 * @param mixed $fields
453 * @param string $cookie
454 * @param integer $count
455 * @return array
456 */
457 private function sentRequest ($path, $fields, $cookie = false, $count = 0) {
458 $url = $this->url.$path.'?lang=en';
459 $ch = curl_init();
460 curl_setopt($ch, CURLOPT_URL, $url);
461
462 if (!empty($fields)) {
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 }
471 }
472
473 if ($cookie === true) {
474 curl_setopt($ch, CURLOPT_COOKIE, 'challengev='.$this->challenge.'; '.$this->cookie);
475 }
476
477 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
478 curl_setopt($ch, CURLOPT_HEADER, true);
479
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
491 // check if body is encrypted (hex instead of json)
492 if (ctype_xdigit($body)) {
493 $body = $this->decrypt($body);
494 }
495
496 // fix invalid json
497 $body = preg_replace("/(\r\n)|(\r)/", "\n", $body);
498 $body = preg_replace('/\'/i', '"', $body);
499 $body = preg_replace("/\[\s+\]/i", '[ {} ]', $body);
500 $body = preg_replace("/},\s+]/", "}\n]", $body);
501
502 return array('header' => $this->parse_headers($header), 'body' => $body);
503 }
504
505 /**
506 * get the csrf_token
507 *
508 * @return string
509 */
510 private function getToken () {
511 $this->checkLogin();
512
513 $path = 'html/content/overview/index.html';
514 $fields = array();
515 $data = $this->sentRequest($path, $fields, true);
516
517 if (empty($data['body'])) {
518 throw new RouterExeption('unable to get csrf_token');
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 {
528 throw new RouterExeption('unable to get csrf_token');
529 }
530 }
531
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));
546 $derivedk = substr($derivedk, 0, 32);
547 }
548 else {
549 $derivedk = hash_pbkdf2('sha1', hash('sha256', $password), substr($this->challenge, 0, 16), 1000, 32);
550 }
551
552 if (empty($derivedk)) {
553 throw new RouterException('unable to calculate derivedk');
554 }
555
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) {
566 $cookie = '';
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])) {
570 $cookie = $match[1];
571 }
572 }
573
574 if (empty($cookie)) {
575 throw new RouterExeption('unable to get the session cookie from the router');
576 }
577
578 return $cookie;
579 }
580
581 /**
582 * parse the curl return header into an array
583 *
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
591 $header_text = explode("\r\n", $header_text);
592 foreach ($header_text as $i => $line) {
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 }
604 }