split the stuff into different files
[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');
aacbbd28 4require_once('CryptLib/CryptLib.php');
9b926efe
S
5require_once('Connection.class.php');
6require_once('Phone.class.php');
7require_once('System.class.php');
8162139f 8
1d934afe
S
9/**
10 * @author Jan Altensen (Stricted)
11 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
12 * @copyright 2015 Jan Altensen (Stricted)
13 */
21f877e4 14class SpeedportHybrid {
9b926efe
S
15 use Connection;
16 use Phone;
17 use System;
18
0dca2d61 19 /**
23cc0061
S
20 * class version
21 * @const string
0dca2d61 22 */
219ba661 23 const VERSION = '1.0.3';
0dca2d61 24
a91317a6
S
25 /**
26 * password-challenge
27 * @var string
28 */
29 private $challenge = '';
30
809e25bd
S
31 /**
32 * csrf_token
33 * @var string
34 */
35 private $token = '';
36
a91317a6
S
37 /**
38 * hashed password
39 * @var string
40 */
41 private $hash = '';
42
43 /**
44 * session cookie
45 * @var string
46 */
ac344b92 47 private $cookie = '';
a91317a6
S
48
49 /**
50 * router url
51 * @var string
52 */
b5a532a8 53 private $url = '';
a91317a6 54
c9e082da
S
55 /**
56 * derivedk cookie
57 * @var string
58 */
59 private $derivedk = '';
60
e58a96d1 61 public function __construct ($url = 'http://speedport.ip/') {
b5a532a8 62 $this->url = $url;
a91317a6
S
63 }
64
65 /**
66 * Requests the password-challenge from the router.
67 */
e58a96d1 68 private function getChallenge () {
b5a532a8 69 $path = 'data/Login.json';
a91317a6 70 $fields = array('csrf_token' => 'nulltoken', 'showpw' => 0, 'challengev' => 'null');
b5a532a8 71 $data = $this->sentRequest($path, $fields);
219ba661 72 $data = $this->getValues($data['body']);
c2678616
S
73
74 if (isset($data['challengev']) && !empty($data['challengev'])) {
ac344b92
S
75 return $data['challengev'];
76 }
77 else {
219ba661 78 throw new RouterException('unable to get the challenge from the router');
a91317a6
S
79 }
80 }
81
82 /**
83 * login into the router with the given password
5e44cffa 84 *
a91317a6
S
85 * @param string $password
86 * @return boolean
87 */
88 public function login ($password) {
ac344b92 89 $this->challenge = $this->getChallenge();
e58a96d1 90
b5a532a8 91 $path = 'data/Login.json';
a91317a6
S
92 $this->hash = hash('sha256', $this->challenge.':'.$password);
93 $fields = array('csrf_token' => 'nulltoken', 'showpw' => 0, 'password' => $this->hash);
b5a532a8 94 $data = $this->sentRequest($path, $fields);
219ba661 95 $json = $this->getValues($data['body']);
ac344b92 96
c2678616 97 if (isset($json['login']) && $json['login'] == 'success') {
ac344b92
S
98 $this->cookie = $this->getCookie($data);
99
100 $this->derivedk = $this->getDerviedk($password);
101
102 // get the csrf_token
103 $this->token = $this->getToken();
104
105 if ($this->checkLogin(false) === true) {
106 return true;
a91317a6
S
107 }
108 }
109
110 return false;
111 }
112
df1e1394
S
113 /**
114 * check if we are logged in
115 *
adcedd8b
S
116 * @param boolean $exception
117 * @return boolean
df1e1394 118 */
adcedd8b 119 public function checkLogin ($exception = true) {
ac344b92
S
120 // check if challenge or session is empty
121 if (empty($this->challenge) || empty($this->cookie)) {
adcedd8b 122 if ($exception === true) {
219ba661 123 throw new RouterException('you musst be logged in to use this method');
adcedd8b
S
124 }
125
0dca2d61
S
126 return false;
127 }
128
34701575 129 $path = 'data/SecureStatus.json';
df1e1394 130 $fields = array();
0dca2d61 131 $data = $this->sentRequest($path, $fields, true);
219ba661 132 $data = $this->getValues($data['body']);
df1e1394 133
219ba661 134 if ($data['loginstate'] != 1) {
adcedd8b 135 if ($exception === true) {
219ba661 136 throw new RouterException('you musst be logged in to use this method');
adcedd8b
S
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';
9b926efe 154 $fields = array('csrf_token' => $this->token, 'logout' => 'byby');
219ba661
S
155 $data = $this->sentRequest($path, $fields, true);
156 $data = $this->getValues($data['body']);
157 if ((isset($data['status']) && $data['status'] == 'ok') && $this->checkLogin(false) === false) {
df1e1394
S
158 // reset challenge and session
159 $this->challenge = '';
ac344b92
S
160 $this->cookie = '';
161 $this->token = '';
162 $this->derivedk = '';
df1e1394 163
58feafa2 164 return true;
df1e1394 165 }
58feafa2
S
166
167 return false;
a91317a6
S
168 }
169
170 /**
171 * reboot the router
5e44cffa 172 *
58feafa2 173 * @return boolean
a91317a6
S
174 */
175 public function reboot () {
adcedd8b 176 $this->checkLogin();
e58a96d1 177
b5a532a8 178 $path = 'data/Reboot.json';
8162139f
S
179 $fields = array('csrf_token' => $this->token, 'reboot_device' => 'true');
180 $data = $this->sentEncryptedRequest($path, $fields, true);
219ba661 181 $data = $this->getValues($data['body']);
14d4f286 182
219ba661 183 if ($data['status'] == 'ok') {
aacbbd28
S
184 // reset challenge and session
185 $this->challenge = '';
186 $this->cookie = '';
187 $this->token = '';
188 $this->derivedk = '';
189
8162139f
S
190 // throw an exception because router is unavailable for other tasks
191 // like $this->logout() or $this->checkLogin
192 throw new RebootException('Router Reboot');
193 }
58feafa2
S
194
195 return false;
a91317a6
S
196 }
197
14d4f286
S
198 /**
199 * decrypt data from router
200 *
201 * @param string $data
202 * @return array
203 */
c2678616 204 private function decrypt ($data) {
14d4f286
S
205 $iv = hex2bin(substr($this->challenge, 16, 16));
206 $adata = hex2bin(substr($this->challenge, 32, 16));
23cc0061 207 $key = hex2bin($this->derivedk);
14d4f286
S
208 $enc = hex2bin($data);
209
23cc0061
S
210 $factory = new CryptLib\Cipher\Factory();
211 $aes = $factory->getBlockCipher('rijndael-128');
212 $aes->setKey($key);
14d4f286
S
213 $mode = $factory->getMode('ccm', $aes, $iv, [ 'adata' => $adata, 'lSize' => 7]);
214
215 $mode->decrypt($enc);
216
217 return $mode->finish();
218 }
219
220 /**
221 * decrypt data for the router
222 *
23cc0061 223 * @param string $data
14d4f286
S
224 * @return string
225 */
c2678616 226 private function encrypt ($data) {
14d4f286
S
227 $iv = hex2bin(substr($this->challenge, 16, 16));
228 $adata = hex2bin(substr($this->challenge, 32, 16));
23cc0061 229 $key = hex2bin($this->derivedk);
14d4f286 230
23cc0061
S
231 $factory = new CryptLib\Cipher\Factory();
232 $aes = $factory->getBlockCipher('rijndael-128');
233 $aes->setKey($key);
14d4f286 234 $mode = $factory->getMode('ccm', $aes, $iv, [ 'adata' => $adata, 'lSize' => 7]);
23cc0061 235 $mode->encrypt($data);
14d4f286 236
809e25bd 237 return bin2hex($mode->finish());
14d4f286
S
238 }
239
c2678616
S
240 /**
241 * get the values from array
242 *
243 * @param array $array
244 * @return array
245 */
246 private function getValues($array) {
247 $data = array();
248 foreach ($array as $item) {
aacbbd28 249 // thank you telekom for this piece of shit
8b7e3972 250 if ($item['vartype'] == 'template') {
ec351a7d
S
251 if (is_array($item['varvalue'])) {
252 $data[$item['varid']][] = $this->getValues($item['varvalue']);
253 }
254 else {
23cc0061 255 // i dont know if we need this
ec351a7d
S
256 $data[$item['varid']] = $item['varvalue'];
257 }
58feafa2
S
258 }
259 else {
8b7e3972
S
260 if (is_array($item['varvalue'])) {
261 $data[$item['varid']] = $this->getValues($item['varvalue']);
262 }
263 else {
264 $data[$item['varid']] = $item['varvalue'];
265 }
58feafa2 266 }
c2678616
S
267 }
268
269 return $data;
270 }
271
8162139f
S
272 /**
273 * sends the encrypted request to router
274 *
275 * @param string $path
276 * @param mixed $fields
277 * @param string $cookie
278 * @return array
279 */
280 private function sentEncryptedRequest ($path, $fields, $cookie = false) {
281 $count = count($fields);
23cc0061 282 $fields = $this->encrypt(http_build_query($fields));
8162139f
S
283 return $this->sentRequest($path, $fields, $cookie, $count);
284 }
285
a91317a6
S
286 /**
287 * sends the request to router
5e44cffa 288 *
b5a532a8 289 * @param string $path
809e25bd 290 * @param mixed $fields
a91317a6 291 * @param string $cookie
809e25bd 292 * @param integer $count
a91317a6
S
293 * @return array
294 */
0dca2d61 295 private function sentRequest ($path, $fields, $cookie = false, $count = 0) {
49abc701 296 $url = $this->url.$path.'?lang=en';
a91317a6
S
297 $ch = curl_init();
298 curl_setopt($ch, CURLOPT_URL, $url);
299
300 if (!empty($fields)) {
809e25bd
S
301 if (is_array($fields)) {
302 curl_setopt($ch, CURLOPT_POST, count($fields));
303 curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($fields));
304 }
305 else {
306 curl_setopt($ch, CURLOPT_POST, $count);
307 curl_setopt($ch, CURLOPT_POSTFIELDS, $fields);
308 }
a91317a6
S
309 }
310
0dca2d61 311 if ($cookie === true) {
559c5be7 312 curl_setopt($ch, CURLOPT_COOKIE, 'challengev='.$this->challenge.'; '.$this->cookie);
a91317a6
S
313 }
314
315 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
316 curl_setopt($ch, CURLOPT_HEADER, true);
317
a91317a6
S
318 $result = curl_exec($ch);
319
320 $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
321 $header = substr($result, 0, $header_size);
322 $body = substr($result, $header_size);
323 curl_close($ch);
324
219ba661
S
325 // check if response is empty
326 if (empty($body)) {
327 throw new RouterException('empty response');
328 }
329
8162139f
S
330 // check if body is encrypted (hex instead of json)
331 if (ctype_xdigit($body)) {
332 $body = $this->decrypt($body);
333 }
334
a91317a6
S
335 // fix invalid json
336 $body = preg_replace("/(\r\n)|(\r)/", "\n", $body);
337 $body = preg_replace('/\'/i', '"', $body);
5e44cffa 338 $body = preg_replace("/\[\s+\]/i", '[ {} ]', $body);
dae16c50 339 $body = preg_replace("/},\s+]/", "}\n]", $body);
a91317a6 340
219ba661 341 // decode json
aacbbd28 342 if (strpos($url, '.json') !== false) {
219ba661
S
343 $body = json_decode($body, true);
344 }
345
a91317a6
S
346 return array('header' => $this->parse_headers($header), 'body' => $body);
347 }
348
809e25bd
S
349 /**
350 * get the csrf_token
351 *
352 * @return string
353 */
354 private function getToken () {
adcedd8b 355 $this->checkLogin();
e58a96d1 356
49abc701 357 $path = 'html/content/overview/index.html';
809e25bd 358 $fields = array();
0dca2d61 359 $data = $this->sentRequest($path, $fields, true);
809e25bd 360
809e25bd
S
361 $a = explode('csrf_token = "', $data['body']);
362 $a = explode('";', $a[1]);
363
364 if (isset($a[0]) && !empty($a[0])) {
365 return $a[0];
366 }
367 else {
219ba661 368 throw new RouterException('unable to get csrf_token');
809e25bd
S
369 }
370 }
371
ac344b92
S
372 /**
373 * calculate the derivedk
374 *
375 * @param string $password
376 * @return string
377 */
378 private function getDerviedk ($password) {
379 $derivedk = '';
380
381 // calculate derivedk
382 if (!function_exists('hash_pbkdf2')) {
ac344b92
S
383 $pbkdf2 = new CryptLib\Key\Derivation\PBKDF\PBKDF2(array('hash' => 'sha1'));
384 $derivedk = bin2hex($pbkdf2->derive(hash('sha256', $password), substr($this->challenge, 0, 16), 1000, 32));
58feafa2 385 $derivedk = substr($derivedk, 0, 32);
ac344b92
S
386 }
387 else {
388 $derivedk = hash_pbkdf2('sha1', hash('sha256', $password), substr($this->challenge, 0, 16), 1000, 32);
389 }
390
58feafa2
S
391 if (empty($derivedk)) {
392 throw new RouterException('unable to calculate derivedk');
393 }
394
ac344b92
S
395 return $derivedk;
396 }
397
398 /**
399 * get cookie from header data
400 *
401 * @param array $data
402 * @return string
403 */
404 private function getCookie ($data) {
58feafa2 405 $cookie = '';
ac344b92
S
406 if (isset($data['header']['Set-Cookie']) && !empty($data['header']['Set-Cookie'])) {
407 preg_match('/^.*(SessionID_R3=[a-z0-9]*).*/i', $data['header']['Set-Cookie'], $match);
408 if (isset($match[1]) && !empty($match[1])) {
58feafa2 409 $cookie = $match[1];
ac344b92
S
410 }
411 }
412
58feafa2 413 if (empty($cookie)) {
219ba661 414 throw new RouterException('unable to get the session cookie from the router');
58feafa2
S
415 }
416
417 return $cookie;
ac344b92
S
418 }
419
a91317a6
S
420 /**
421 * parse the curl return header into an array
5e44cffa 422 *
a91317a6
S
423 * @param string $response
424 * @return array
425 */
426 private function parse_headers($response) {
427 $headers = array();
428 $header_text = substr($response, 0, strpos($response, "\r\n\r\n"));
429
7f4a51d2
S
430 $header_text = explode("\r\n", $header_text);
431 foreach ($header_text as $i => $line) {
a91317a6
S
432 if ($i === 0) {
433 $headers['http_code'] = $line;
434 }
435 else {
436 list ($key, $value) = explode(': ', $line);
437 $headers[$key] = $value;
438 }
439 }
440
441 return $headers;
442 }
7f4a51d2 443}