move some stuff into own functions and rename some variables
[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 array
149 */
150 public function logout () {
151 $this->checkLogin();
152
153 $path = 'data/Login.json';
154 $fields = array('csrf_token' => $this->token, 'logout' => 'byby');
155 $data = $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 $json = json_decode($data['body'], true);
164
165 return $json;
166 }
167 else {
168 throw new RouterExeption('logout failed');
169 }
170 }
171
172 /**
173 * reboot the router
174 *
175 * @return array
176 */
177 public function reboot () {
178 $this->checkLogin();
179
180 $path = 'data/Reboot.json';
181 $fields = array('csrf_token' => $this->token, 'reboot_device' => 'true');
182 $data = $this->sentEncryptedRequest($path, $fields, true);
183
184 $json = json_decode($data['body'], true);
185 $json = $this->getValues($json);
186
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 }
195 }
196
197 /**
198 * change dsl connection status
199 *
200 * @param string $status
201 */
202 public function changeConnectionStatus ($status) {
203 $this->checkLogin();
204
205 $path = 'data/Connect.json';
206
207 if ($status == 'online' || $status == 'offline') {
208 $fields = array('csrf_token' => 'nulltoken', 'showpw' => 0, 'password' => $this->hash, 'req_connect' => $status);
209 $data = $this->sentRequest($path, $fields, true);
210
211 $json = json_decode($data['body'], true);
212
213 return $json;
214 }
215 else {
216 throw new RouterExeption();
217 }
218 }
219
220 /**
221 * return the given json as array
222 *
223 * @param string $file
224 * @return array
225 */
226 public function getData ($file) {
227 if ($file != 'Status') $this->checkLogin();
228
229 $path = 'data/'.$file.'.json';
230 $fields = array();
231 $data = $this->sentRequest($path, $fields, true);
232
233 if (empty($data['body'])) {
234 throw new RouterExeption('unable to get '.$file.' data');
235 }
236
237 $json = json_decode($data['body'], true);
238
239 return $json;
240 }
241
242 /**
243 * get the router syslog
244 *
245 * @return array
246 */
247 public function getSyslog() {
248 return $this->exportData('0');
249 }
250
251 /**
252 * get the Missed Calls from router
253 *
254 * @return array
255 */
256 public function getMissedCalls() {
257 return $this->exportData('1');
258 }
259
260 /**
261 * get the Taken Calls from router
262 *
263 * @return array
264 */
265 public function getTakenCalls() {
266 return $this->exportData('2');
267 }
268
269 /**
270 * get the Dialed Calls from router
271 *
272 * @return array
273 */
274 public function getDialedCalls() {
275 return $this->exportData('3');
276 }
277
278 /**
279 * export data from router
280 *
281 * @return array
282 */
283 private function exportData ($type) {
284 $this->checkLogin();
285
286 $path = 'data/Syslog.json';
287 $fields = array('exporttype' => $type);
288 $data = $this->sentRequest($path, $fields, true);
289
290 if (empty($data['body'])) {
291 throw new RouterExeption('unable to get export data');
292 }
293
294 return explode("\n", $data['body']);
295 }
296
297 /**
298 * reconnect LTE
299 *
300 * @return array
301 */
302 public function reconnectLte () {
303 $this->checkLogin();
304
305 $path = 'data/modules.json';
306 $fields = array('csrf_token' => $this->token, 'lte_reconn' => '1');
307 $data = $this->sentEncryptedRequest($path, $fields, true);
308 $json = json_decode($data['body'], true);
309
310 return $json;
311 }
312
313 /**
314 * reset the router to Factory Default
315 * not tested
316 *
317 * @return array
318 */
319 public function resetToFactoryDefault () {
320 $this->checkLogin();
321
322 $path = 'data/resetAllSetting.json';
323 $fields = array('csrf_token' => 'nulltoken', 'showpw' => 0, 'password' => $this->hash, 'reset_all' => 'true');
324 $data = $this->sentRequest($path, $fields, true);
325 $json = json_decode($data['body'], true);
326
327 return $json;
328 }
329
330
331 /**
332 * check if firmware is actual
333 *
334 * @return array
335 */
336 public function checkFirmware () {
337 $this->checkLogin();
338
339 $path = 'data/checkfirmware.json';
340 $fields = array('checkfirmware' => 'true');
341 $data = $this->sentRequest($path, $fields, true);
342
343 if (empty($data['body'])) {
344 throw new RouterExeption('unable to get checkfirmware data');
345 }
346
347 $json = json_decode($data['body'], true);
348
349 return $json;
350 }
351
352 /**
353 * decrypt data from router
354 *
355 * @param string $data
356 * @return array
357 */
358 private function decrypt ($data) {
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 */
382 private function encrypt ($data) {
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
395 return bin2hex($mode->finish());
396 }
397
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
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
427 /**
428 * sends the request to router
429 *
430 * @param string $path
431 * @param mixed $fields
432 * @param string $cookie
433 * @param integer $count
434 * @return array
435 */
436 private function sentRequest ($path, $fields, $cookie = false, $count = 0) {
437 $url = $this->url.$path.'?lang=en';
438 $ch = curl_init();
439 curl_setopt($ch, CURLOPT_URL, $url);
440
441 if (!empty($fields)) {
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 }
450 }
451
452 if ($cookie === true) {
453 curl_setopt($ch, CURLOPT_COOKIE, 'challengev='.$this->challenge.'; '.$this->cookie);
454 }
455
456 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
457 curl_setopt($ch, CURLOPT_HEADER, true);
458
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
470 // check if body is encrypted (hex instead of json)
471 if (ctype_xdigit($body)) {
472 $body = $this->decrypt($body);
473 }
474
475 // fix invalid json
476 $body = preg_replace("/(\r\n)|(\r)/", "\n", $body);
477 $body = preg_replace('/\'/i', '"', $body);
478 $body = preg_replace("/\[\s+\]/i", '[ {} ]', $body);
479 $body = preg_replace("/},\s+]/", "}\n]", $body);
480
481 return array('header' => $this->parse_headers($header), 'body' => $body);
482 }
483
484 /**
485 * get the csrf_token
486 *
487 * @return string
488 */
489 private function getToken () {
490 $this->checkLogin();
491
492 $path = 'html/content/overview/index.html';
493 $fields = array();
494 $data = $this->sentRequest($path, $fields, true);
495
496 if (empty($data['body'])) {
497 throw new RouterExeption('unable to get csrf_token');
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 {
507 throw new RouterExeption('unable to get csrf_token');
508 }
509 }
510
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
551 /**
552 * parse the curl return header into an array
553 *
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
561 $header_text = explode("\r\n", $header_text);
562 foreach ($header_text as $i => $line) {
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 }
574 }