892355a252ccac363dfcd17d6afe21d3a9338258
[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 require_once('CryptLib/CryptLib.php');
5 require_once('Connection.class.php');
6 require_once('Phone.class.php');
7 require_once('System.class.php');
8
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 */
14 class SpeedportHybrid {
15 use Connection;
16 use Phone;
17 use System;
18
19 /**
20 * class version
21 * @const string
22 */
23 const VERSION = '1.0.3';
24
25 /**
26 * password-challenge
27 * @var string
28 */
29 private $challenge = '';
30
31 /**
32 * csrf_token
33 * @var string
34 */
35 private $token = '';
36
37 /**
38 * hashed password
39 * @var string
40 */
41 private $hash = '';
42
43 /**
44 * session cookie
45 * @var string
46 */
47 private $cookie = '';
48
49 /**
50 * router url
51 * @var string
52 */
53 private $url = '';
54
55 /**
56 * derivedk cookie
57 * @var string
58 */
59 private $derivedk = '';
60
61 public function __construct ($url = 'http://speedport.ip/') {
62 $this->url = $url;
63 }
64
65 /**
66 * Requests the password-challenge from the router.
67 */
68 private function getChallenge () {
69 $path = 'data/Login.json';
70 $fields = array('csrf_token' => 'nulltoken', 'showpw' => 0, 'challengev' => 'null');
71 $data = $this->sentRequest($path, $fields);
72 $data = $this->getValues($data['body']);
73
74 if (isset($data['challengev']) && !empty($data['challengev'])) {
75 return $data['challengev'];
76 }
77 else {
78 throw new RouterException('unable to get the challenge from the router');
79 }
80 }
81
82 /**
83 * login into the router with the given password
84 *
85 * @param string $password
86 * @return boolean
87 */
88 public function login ($password) {
89 $this->challenge = $this->getChallenge();
90
91 $path = 'data/Login.json';
92 $this->hash = hash('sha256', $this->challenge.':'.$password);
93 $fields = array('csrf_token' => 'nulltoken', 'showpw' => 0, 'password' => $this->hash);
94 $data = $this->sentRequest($path, $fields);
95 $json = $this->getValues($data['body']);
96
97 if (isset($json['login']) && $json['login'] == 'success') {
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;
107 }
108 }
109
110 return false;
111 }
112
113 /**
114 * check if we are logged in
115 *
116 * @param boolean $exception
117 * @return boolean
118 */
119 public function checkLogin ($exception = true) {
120 // check if challenge or session is empty
121 if (empty($this->challenge) || empty($this->cookie)) {
122 if ($exception === true) {
123 throw new RouterException('you musst be logged in to use this method');
124 }
125
126 return false;
127 }
128
129 $path = 'data/SecureStatus.json';
130 $fields = array();
131 $data = $this->sentRequest($path, $fields, true);
132 $data = $this->getValues($data['body']);
133
134 if ($data['loginstate'] != 1) {
135 if ($exception === true) {
136 throw new RouterException('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 $data = $this->sentRequest($path, $fields, true);
156 $data = $this->getValues($data['body']);
157 if ((isset($data['status']) && $data['status'] == 'ok') && $this->checkLogin(false) === false) {
158 // reset challenge and session
159 $this->challenge = '';
160 $this->cookie = '';
161 $this->token = '';
162 $this->derivedk = '';
163
164 return true;
165 }
166
167 return false;
168 }
169
170 /**
171 * reboot the router
172 *
173 * @return boolean
174 */
175 public function reboot () {
176 $this->checkLogin();
177
178 $path = 'data/Reboot.json';
179 $fields = array('csrf_token' => $this->token, 'reboot_device' => 'true');
180 $data = $this->sentEncryptedRequest($path, $fields, true);
181 $data = $this->getValues($data['body']);
182
183 if ($data['status'] == 'ok') {
184 // reset challenge and session
185 $this->challenge = '';
186 $this->cookie = '';
187 $this->token = '';
188 $this->derivedk = '';
189
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 }
194
195 return false;
196 }
197
198 /**
199 * decrypt data from router
200 *
201 * @param string $data
202 * @return array
203 */
204 private function decrypt ($data) {
205 $iv = hex2bin(substr($this->challenge, 16, 16));
206 $adata = hex2bin(substr($this->challenge, 32, 16));
207 $key = hex2bin($this->derivedk);
208 $enc = hex2bin($data);
209
210 $factory = new CryptLib\Cipher\Factory();
211 $aes = $factory->getBlockCipher('rijndael-128');
212 $aes->setKey($key);
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 *
223 * @param string $data
224 * @return string
225 */
226 private function encrypt ($data) {
227 $iv = hex2bin(substr($this->challenge, 16, 16));
228 $adata = hex2bin(substr($this->challenge, 32, 16));
229 $key = hex2bin($this->derivedk);
230
231 $factory = new CryptLib\Cipher\Factory();
232 $aes = $factory->getBlockCipher('rijndael-128');
233 $aes->setKey($key);
234 $mode = $factory->getMode('ccm', $aes, $iv, [ 'adata' => $adata, 'lSize' => 7]);
235 $mode->encrypt($data);
236
237 return bin2hex($mode->finish());
238 }
239
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) {
249 // thank you telekom for this piece of shit
250 if ($item['vartype'] == 'template') {
251 if (is_array($item['varvalue'])) {
252 $data[$item['varid']][] = $this->getValues($item['varvalue']);
253 }
254 else {
255 // i dont know if we need this
256 $data[$item['varid']] = $item['varvalue'];
257 }
258 }
259 else {
260 if (is_array($item['varvalue'])) {
261 $data[$item['varid']] = $this->getValues($item['varvalue']);
262 }
263 else {
264 $data[$item['varid']] = $item['varvalue'];
265 }
266 }
267 }
268
269 return $data;
270 }
271
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);
282 $fields = $this->encrypt(http_build_query($fields));
283 return $this->sentRequest($path, $fields, $cookie, $count);
284 }
285
286 /**
287 * sends the request to router
288 *
289 * @param string $path
290 * @param mixed $fields
291 * @param string $cookie
292 * @param integer $count
293 * @return array
294 */
295 private function sentRequest ($path, $fields, $cookie = false, $count = 0) {
296 $url = $this->url.$path.'?lang=en';
297 $ch = curl_init();
298 curl_setopt($ch, CURLOPT_URL, $url);
299
300 if (!empty($fields)) {
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 }
309 }
310
311 if ($cookie === true) {
312 curl_setopt($ch, CURLOPT_COOKIE, 'challengev='.$this->challenge.'; '.$this->cookie);
313 }
314
315 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
316 curl_setopt($ch, CURLOPT_HEADER, true);
317
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
325 // check if response is empty
326 if (empty($body)) {
327 throw new RouterException('empty response');
328 }
329
330 // check if body is encrypted (hex instead of json)
331 if (ctype_xdigit($body)) {
332 $body = $this->decrypt($body);
333 }
334
335 // fix invalid json
336 $body = preg_replace("/(\r\n)|(\r)/", "\n", $body);
337 $body = preg_replace('/\'/i', '"', $body);
338 $body = preg_replace("/\[\s+\]/i", '[ {} ]', $body);
339 $body = preg_replace("/},\s+]/", "}\n]", $body);
340
341 // decode json
342 if (strpos($url, '.json') !== false) {
343 $body = json_decode($body, true);
344 }
345
346 return array('header' => $this->parse_headers($header), 'body' => $body);
347 }
348
349 /**
350 * get the csrf_token
351 *
352 * @return string
353 */
354 private function getToken () {
355 $this->checkLogin();
356
357 $path = 'html/content/overview/index.html';
358 $fields = array();
359 $data = $this->sentRequest($path, $fields, true);
360
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 {
368 throw new RouterException('unable to get csrf_token');
369 }
370 }
371
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')) {
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));
385 $derivedk = substr($derivedk, 0, 32);
386 }
387 else {
388 $derivedk = hash_pbkdf2('sha1', hash('sha256', $password), substr($this->challenge, 0, 16), 1000, 32);
389 }
390
391 if (empty($derivedk)) {
392 throw new RouterException('unable to calculate derivedk');
393 }
394
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) {
405 $cookie = '';
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])) {
409 $cookie = $match[1];
410 }
411 }
412
413 if (empty($cookie)) {
414 throw new RouterException('unable to get the session cookie from the router');
415 }
416
417 return $cookie;
418 }
419
420 /**
421 * parse the curl return header into an array
422 *
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
430 $header_text = explode("\r\n", $header_text);
431 foreach ($header_text as $i => $line) {
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 }
443 }