improve checkLogin
[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 $session = '';
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 $this->challenge = $data['challengev'];
69 }
70 }
71
72 /**
73 * login into the router with the given password
74 *
75 * @param string $password
76 * @return boolean
77 */
78 public function login ($password) {
79 $this->getChallenge();
80
81 if (empty($this->challenge)) {
82 throw new RouterExeption('unable to get the challenge from the router');
83 }
84
85 $path = 'data/Login.json';
86 $this->hash = hash('sha256', $this->challenge.':'.$password);
87 $fields = array('csrf_token' => 'nulltoken', 'showpw' => 0, 'password' => $this->hash);
88 $data = $this->sentRequest($path, $fields);
89 $json = json_decode($data['body'], true);
90 $json = $this->getValues($json);
91 if (isset($json['login']) && $json['login'] == 'success') {
92 if (isset($data['header']['Set-Cookie']) && !empty($data['header']['Set-Cookie'])) {
93 preg_match('/^.*(SessionID_R3=[a-z0-9]*).*/i', $data['header']['Set-Cookie'], $match);
94 if (isset($match[1]) && !empty($match[1])) {
95 $this->session = $match[1];
96 }
97 else {
98 throw new RouterExeption('unable to get the session cookie from the router');
99 }
100
101 // calculate derivedk
102 if (!function_exists("hash_pbkdf2")) {
103 require_once 'CryptLib/CryptLib.php';
104 $pbkdf2 = new CryptLib\Key\Derivation\PBKDF\PBKDF2(array('hash' => 'sha1'));
105 $this->derivedk = bin2hex($pbkdf2->derive(hash('sha256', $password), substr($this->challenge, 0, 16), 1000, 32));
106 $this->derivedk = substr($this->derivedk, 0, 32);
107 }
108 else {
109 $this->derivedk = hash_pbkdf2('sha1', hash('sha256', $password), substr($this->challenge, 0, 16), 1000, 32);
110 }
111
112 // get the csrf_token
113 $this->token = $this->getToken();
114
115 if ($this->checkLogin(false) === true) {
116 return true;
117 }
118 }
119 }
120
121 return false;
122 }
123
124 /**
125 * check if we are logged in
126 *
127 * @param boolean $exception
128 * @return boolean
129 */
130 public function checkLogin ($exception = true) {
131 if (empty($this->challenge) && empty($this->session)) {
132 if ($exception === true) {
133 throw new RouterExeption('you musst be logged in to use this method');
134 }
135
136 return false;
137 }
138
139 $path = 'data/SecureStatus.json';
140 $fields = array();
141 $data = $this->sentRequest($path, $fields, true);
142
143 if (empty($data['body'])) {
144 throw new RouterExeption('unable to get SecureStatus data');
145 }
146
147 $json = json_decode($data['body'], true);
148 $json = $this->getValues($json);
149
150 if ($json['loginstate'] != 1) {
151 if ($exception === true) {
152 throw new RouterExeption('you musst be logged in to use this method');
153 }
154
155 return false;
156 }
157
158 return true;
159 }
160
161 /**
162 * logout
163 *
164 * @return array
165 */
166 public function logout () {
167 $this->checkLogin();
168
169 $path = 'data/Login.json';
170 $fields = array('csrf_token' => $this->token, 'logout' => 'byby');
171 $data = $this->sentRequest($path, $fields, true);
172 if ($this->checkLogin(false) === false) {
173 // reset challenge and session
174 $this->challenge = '';
175 $this->session = '';
176 $this->token = "";
177
178 $json = json_decode($data['body'], true);
179
180 return $json;
181 }
182 else {
183 throw new RouterExeption('logout failed');
184 }
185 }
186
187 /**
188 * reboot the router
189 *
190 * @return array
191 */
192 public function reboot () {
193 $this->checkLogin();
194
195 $path = 'data/Reboot.json';
196 $fields = array('csrf_token' => $this->token, 'reboot_device' => 'true');
197 $data = $this->sentEncryptedRequest($path, $fields, true);
198
199 $json = json_decode($data['body'], true);
200 $json = $this->getValues($json);
201
202 if ($json['status'] == 'ok') {
203 // throw an exception because router is unavailable for other tasks
204 // like $this->logout() or $this->checkLogin
205 throw new RebootException('Router Reboot');
206 }
207 else {
208 throw new RouterException('unable to reboot');
209 }
210 }
211
212 /**
213 * change dsl connection status
214 *
215 * @param string $status
216 */
217 public function changeConnectionStatus ($status) {
218 $this->checkLogin();
219
220 $path = 'data/Connect.json';
221
222 if ($status == 'online' || $status == 'offline') {
223 $fields = array('csrf_token' => 'nulltoken', 'showpw' => 0, 'password' => $this->hash, 'req_connect' => $status);
224 $data = $this->sentRequest($path, $fields, true);
225
226 $json = json_decode($data['body'], true);
227
228 return $json;
229 }
230 else {
231 throw new RouterExeption();
232 }
233 }
234
235 /**
236 * return the given json as array
237 *
238 * @param string $file
239 * @return array
240 */
241 public function getData ($file) {
242 if ($file != "Status") $this->checkLogin();
243
244 $path = 'data/'.$file.'.json';
245 $fields = array();
246 $data = $this->sentRequest($path, $fields, true);
247
248 if (empty($data['body'])) {
249 throw new RouterExeption('unable to get '.$file.' data');
250 }
251
252 $json = json_decode($data['body'], true);
253
254 return $json;
255 }
256
257 /**
258 * get the router syslog
259 *
260 * @return array
261 */
262 public function getSyslog() {
263 return $this->exportData('0');
264 }
265
266 /**
267 * get the Missed Calls from router
268 *
269 * @return array
270 */
271 public function getMissedCalls() {
272 return $this->exportData('1');
273 }
274
275 /**
276 * get the Taken Calls from router
277 *
278 * @return array
279 */
280 public function getTakenCalls() {
281 return $this->exportData('2');
282 }
283
284 /**
285 * get the Dialed Calls from router
286 *
287 * @return array
288 */
289 public function getDialedCalls() {
290 return $this->exportData('3');
291 }
292
293 /**
294 * export data from router
295 *
296 * @return array
297 */
298 private function exportData ($type) {
299 $this->checkLogin();
300
301 $path = 'data/Syslog.json';
302 $fields = array('exporttype' => $type);
303 $data = $this->sentRequest($path, $fields, true);
304
305 if (empty($data['body'])) {
306 throw new RouterExeption('unable to get export data');
307 }
308
309 return explode("\n", $data['body']);
310 }
311
312 /**
313 * reconnect LTE
314 *
315 * @return array
316 */
317 public function reconnectLte () {
318 $this->checkLogin();
319
320 $path = 'data/modules.json';
321 $fields = array('csrf_token' => $this->token, 'lte_reconn' => '1');
322 $data = $this->sentEncryptedRequest($path, $fields, true);
323 $json = json_decode($data['body'], true);
324
325 return $json;
326 }
327
328 /**
329 * reset the router to Factory Default
330 * not tested
331 *
332 * @return array
333 */
334 public function resetToFactoryDefault () {
335 $this->checkLogin();
336
337 $path = 'data/resetAllSetting.json';
338 $fields = array('csrf_token' => 'nulltoken', 'showpw' => 0, 'password' => $this->hash, 'reset_all' => 'true');
339 $data = $this->sentRequest($path, $fields, true);
340 $json = json_decode($data['body'], true);
341
342 return $json;
343 }
344
345
346 /**
347 * check if firmware is actual
348 *
349 * @return array
350 */
351 public function checkFirmware () {
352 $this->checkLogin();
353
354 $path = 'data/checkfirmware.json';
355 $fields = array('checkfirmware' => 'true');
356 $data = $this->sentRequest($path, $fields, true);
357
358 if (empty($data['body'])) {
359 throw new RouterExeption('unable to get checkfirmware data');
360 }
361
362 $json = json_decode($data['body'], true);
363
364 return $json;
365 }
366
367 /**
368 * decrypt data from router
369 *
370 * @param string $data
371 * @return array
372 */
373 private function decrypt ($data) {
374 require_once 'CryptLib/CryptLib.php';
375 $factory = new CryptLib\Cipher\Factory();
376 $aes = $factory->getBlockCipher('rijndael-128');
377
378 $iv = hex2bin(substr($this->challenge, 16, 16));
379 $adata = hex2bin(substr($this->challenge, 32, 16));
380 $dkey = hex2bin($this->derivedk);
381 $enc = hex2bin($data);
382
383 $aes->setKey($dkey);
384 $mode = $factory->getMode('ccm', $aes, $iv, [ 'adata' => $adata, 'lSize' => 7]);
385
386 $mode->decrypt($enc);
387
388 return $mode->finish();
389 }
390
391 /**
392 * decrypt data for the router
393 *
394 * @param array $data
395 * @return string
396 */
397 private function encrypt ($data) {
398 require_once 'CryptLib/CryptLib.php';
399 $factory = new CryptLib\Cipher\Factory();
400 $aes = $factory->getBlockCipher('rijndael-128');
401
402 $iv = hex2bin(substr($this->challenge, 16, 16));
403 $adata = hex2bin(substr($this->challenge, 32, 16));
404 $dkey = hex2bin($this->derivedk);
405
406 $aes->setKey($dkey);
407 $mode = $factory->getMode('ccm', $aes, $iv, [ 'adata' => $adata, 'lSize' => 7]);
408 $mode->encrypt(http_build_query($data));
409
410 return bin2hex($mode->finish());
411 }
412
413 /**
414 * get the values from array
415 *
416 * @param array $array
417 * @return array
418 */
419 private function getValues($array) {
420 $data = array();
421 foreach ($array as $item) {
422 $data[$item['varid']] = $item['varvalue'];
423 }
424
425 return $data;
426 }
427
428 /**
429 * sends the encrypted request to router
430 *
431 * @param string $path
432 * @param mixed $fields
433 * @param string $cookie
434 * @return array
435 */
436 private function sentEncryptedRequest ($path, $fields, $cookie = false) {
437 $count = count($fields);
438 $fields = $this->encrypt($fields);
439 return $this->sentRequest($path, $fields, $cookie, $count);
440 }
441
442 /**
443 * sends the request to router
444 *
445 * @param string $path
446 * @param mixed $fields
447 * @param string $cookie
448 * @param integer $count
449 * @return array
450 */
451 private function sentRequest ($path, $fields, $cookie = false, $count = 0) {
452 $url = $this->url.$path.'?lang=en';
453 $ch = curl_init();
454 curl_setopt($ch, CURLOPT_URL, $url);
455
456 if (!empty($fields)) {
457 if (is_array($fields)) {
458 curl_setopt($ch, CURLOPT_POST, count($fields));
459 curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($fields));
460 }
461 else {
462 curl_setopt($ch, CURLOPT_POST, $count);
463 curl_setopt($ch, CURLOPT_POSTFIELDS, $fields);
464 }
465 }
466
467 if ($cookie === true) {
468 curl_setopt($ch, CURLOPT_COOKIE, 'challengev='.$this->challenge.'; '.$this->session);
469 }
470
471 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
472 curl_setopt($ch, CURLOPT_HEADER, true);
473
474 if ($cookie) {
475
476 }
477
478 $result = curl_exec($ch);
479
480 $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
481 $header = substr($result, 0, $header_size);
482 $body = substr($result, $header_size);
483 curl_close($ch);
484
485 // check if body is encrypted (hex instead of json)
486 if (ctype_xdigit($body)) {
487 $body = $this->decrypt($body);
488 }
489
490 // fix invalid json
491 $body = preg_replace("/(\r\n)|(\r)/", "\n", $body);
492 $body = preg_replace('/\'/i', '"', $body);
493 $body = preg_replace("/\[\s+\]/i", '[ {} ]', $body);
494 $body = preg_replace("/},\s+]/", "}\n]", $body);
495
496 return array('header' => $this->parse_headers($header), 'body' => $body);
497 }
498
499 /**
500 * get the csrf_token
501 *
502 * @return string
503 */
504 private function getToken () {
505 $this->checkLogin();
506
507 $path = 'html/content/overview/index.html';
508 $fields = array();
509 $data = $this->sentRequest($path, $fields, true);
510
511 if (empty($data['body'])) {
512 throw new RouterExeption('unable to get csrf_token');
513 }
514
515 $a = explode('csrf_token = "', $data['body']);
516 $a = explode('";', $a[1]);
517
518 if (isset($a[0]) && !empty($a[0])) {
519 return $a[0];
520 }
521 else {
522 throw new RouterExeption('unable to get csrf_token');
523 }
524 }
525
526 /**
527 * parse the curl return header into an array
528 *
529 * @param string $response
530 * @return array
531 */
532 private function parse_headers($response) {
533 $headers = array();
534 $header_text = substr($response, 0, strpos($response, "\r\n\r\n"));
535
536 $header_text = explode("\r\n", $header_text);
537 foreach ($header_text as $i => $line) {
538 if ($i === 0) {
539 $headers['http_code'] = $line;
540 }
541 else {
542 list ($key, $value) = explode(': ', $line);
543 $headers[$key] = $value;
544 }
545 }
546
547 return $headers;
548 }
549 }