add methods to decrypt return data from router
[GitHub/Stricted/speedport-hybrid-php-api.git] / speedport.class.php
CommitLineData
a91317a6 1<?php
1d934afe
S
2/**
3 * @author Jan Altensen (Stricted)
4 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
5 * @copyright 2015 Jan Altensen (Stricted)
6 */
a91317a6
S
7class speedport {
8 /**
9 * password-challenge
10 * @var string
11 */
12 private $challenge = '';
13
14 /**
15 * hashed password
16 * @var string
17 */
18 private $hash = '';
19
20 /**
21 * session cookie
22 * @var string
23 */
24 private $session = '';
25
26 /**
27 * router url
28 * @var string
29 */
b5a532a8 30 private $url = '';
a91317a6 31
c9e082da
S
32 /**
33 * derivedk cookie
34 * @var string
35 */
36 private $derivedk = '';
37
b5a532a8
S
38 public function __construct ($password, $url = 'http://speedport.ip/') {
39 $this->url = $url;
a91317a6
S
40 $this->getChallenge();
41
42 if (empty($this->challenge)) {
43 throw new Exception('unable to get the challenge from the router');
44 }
45
46 $login = $this->login($password);
47
48 if ($login === false) {
49 throw new Exception('unable to login');
50 }
51 }
52
53 /**
54 * Requests the password-challenge from the router.
55 */
56 public function getChallenge () {
b5a532a8 57 $path = 'data/Login.json';
a91317a6 58 $fields = array('csrf_token' => 'nulltoken', 'showpw' => 0, 'challengev' => 'null');
b5a532a8 59 $data = $this->sentRequest($path, $fields);
a91317a6
S
60 $data = json_decode($data['body'], true);
61 if ($data[1]['varid'] == 'challengev') {
62 $this->challenge = $data[1]['varvalue'];
63 }
64 }
65
66 /**
67 * login into the router with the given password
5e44cffa 68 *
a91317a6
S
69 * @param string $password
70 * @return boolean
71 */
72 public function login ($password) {
b5a532a8 73 $path = 'data/Login.json';
a91317a6
S
74 $this->hash = hash('sha256', $this->challenge.':'.$password);
75 $fields = array('csrf_token' => 'nulltoken', 'showpw' => 0, 'password' => $this->hash);
b5a532a8 76 $data = $this->sentRequest($path, $fields);
a91317a6
S
77 $json = json_decode($data['body'], true);
78 if ($json[15]['varid'] == 'login' && $json[15]['varvalue'] == 'success') {
79 if (isset($data['header']['Set-Cookie']) && !empty($data['header']['Set-Cookie'])) {
80 preg_match('/^.*(SessionID_R3=[a-z0-9]*).*/i', $data['header']['Set-Cookie'], $match);
81 if (isset($match[1]) && !empty($match[1])) {
82 $this->session = $match[1];
83 }
84 else {
85 throw new Exception('unable to get the session cookie from the router');
86 }
87
c9e082da
S
88 // calculate derivedk
89 $this->derivedk = hash_pbkdf2('sha1', hash('sha256', $password), substr($this->challenge, 0, 16), 1000, 32);
90
a91317a6
S
91 return true;
92 }
93 }
94
95 return false;
96 }
97
98 /**
99 * logout
5e44cffa 100 *
a91317a6
S
101 * @return array
102 */
103 public function logout () {
b5a532a8 104 $path = 'data/Login.json';
a91317a6 105 $fields = array('logout' => 'byby');
b5a532a8 106 $data = $this->sentRequest($path, $fields);
a91317a6
S
107 // reset challenge and session
108 $this->challenge = '';
109 $this->session = '';
110
111 $json = json_decode($data['body'], true);
112
113 return $json;
114 }
115
116 /**
117 * reboot the router
5e44cffa 118 *
a91317a6
S
119 * @return array
120 */
121 public function reboot () {
b5a532a8 122 $path = 'data/Reboot.json';
a91317a6
S
123 $fields = array('csrf_token' => 'nulltoken', 'showpw' => 0, 'password' => $this->hash, 'reboot_device' => 'true');
124 $cookie = 'challengev='.$this->challenge.'; '.$this->session;
b5a532a8 125 $data = $this->sentRequest($path, $fields, $cookie);
14d4f286 126
a91317a6
S
127 $json = json_decode($data['body'], true);
128
129 return $json;
130 }
131
87026df3
S
132 /**
133 * change dsl connection status
5e44cffa 134 *
87026df3
S
135 * @param string $status
136 */
137 public function changeConnectionStatus ($status) {
b5a532a8 138 $path = 'data/Connect.json';
87026df3
S
139
140 if ($status == 'online' || $status == 'offline') {
141 $fields = array('csrf_token' => 'nulltoken', 'showpw' => 0, 'password' => $this->hash, 'req_connect' => $status);
142 $cookie = 'challengev='.$this->challenge.'; '.$this->session;
b5a532a8 143 $this->sentRequest($path, $fields, $cookie);
87026df3
S
144 }
145 else {
146 throw new Exception();
147 }
148 }
149
a91317a6
S
150 /**
151 * return the given json as array
5e44cffa 152 *
a91317a6
S
153 * the following paths are known to be valid:
154 * /data/dsl.json
155 * /data/interfaces.json
156 * /data/arp.json
157 * /data/session.json
158 * /data/dhcp_client.json
159 * /data/dhcp_server.json
160 * /data/ipv6.json
161 * /data/dns.json
162 * /data/routing.json
163 * /data/igmp_proxy.json
164 * /data/igmp_snooping.json
165 * /data/wlan.json
166 * /data/module.json
167 * /data/memory.json
168 * /data/speed.json
169 * /data/webdav.json
170 * /data/bonding_client.json
171 * /data/bonding_tunnel.json
172 * /data/filterlist.json
173 * /data/bonding_tr181.json
174 * /data/letinfo.json
5e44cffa 175 *
a91317a6 176 * /data/Status.json (No login needed)
5e44cffa 177 *
a91317a6
S
178 * @param string $file
179 * @return array
180 */
181 public function getData ($file) {
b5a532a8 182 $path = 'data/'.$file.'.json';
a91317a6
S
183 $fields = array();
184 $cookie = 'challengev='.$this->challenge.'; '.$this->session;
b5a532a8 185 $data = $this->sentRequest($path, $fields, $cookie);
a91317a6
S
186
187 if (empty($data['body'])) {
188 throw new Exception('unable to get '.$file.' data');
189 }
190
191 $json = json_decode($data['body'], true);
192
193 return $json;
194 }
195
5e44cffa
S
196 /**
197 * get the router syslog
198 *
199 * @return array
200 */
201 public function getSyslog() {
202 $path = 'data/Syslog.json';
203 $fields = array('exporttype' => '0');
204 $cookie = 'challengev='.$this->challenge.'; '.$this->session;
205 $data = $this->sentRequest($path, $fields, $cookie);
206
207 if (empty($data['body'])) {
208 throw new Exception('unable to get syslog data');
209 }
210
211 return explode("\n", $data['body']);
212 }
213
a35e3e67
S
214 /**
215 * get the Missed Calls from router
216 *
217 * @return array
218 */
219 public function getMissedCalls() {
abf641bb 220 $path = 'data/ExportMissedCalls.json';
a35e3e67
S
221 $fields = array('exporttype' => '1');
222 $cookie = 'challengev='.$this->challenge.'; '.$this->session;
223 $data = $this->sentRequest($path, $fields, $cookie);
224
225 if (empty($data['body'])) {
226 throw new Exception('unable to get syslog data');
227 }
228
229 return explode("\n", $data['body']);
230 }
231
15260eeb
S
232 /**
233 * get the Taken Calls from router
234 *
235 * @return array
236 */
237 public function getTakenCalls() {
abf641bb 238 $path = 'data/ExportTakenCalls.json';
15260eeb
S
239 $fields = array('exporttype' => '2');
240 $cookie = 'challengev='.$this->challenge.'; '.$this->session;
241 $data = $this->sentRequest($path, $fields, $cookie);
242
243 if (empty($data['body'])) {
244 throw new Exception('unable to get syslog data');
245 }
246
247 return explode("\n", $data['body']);
248 }
249
abf641bb
S
250 /**
251 * get the Dialed Calls from router
252 *
253 * @return array
254 */
255 public function getDialedCalls() {
256 $path = 'data/ExportDialedCalls.json';
257 $fields = array('exporttype' => '3');
258 $cookie = 'challengev='.$this->challenge.'; '.$this->session;
259 $data = $this->sentRequest($path, $fields, $cookie);
9bd25bb4 260
abf641bb
S
261 if (empty($data['body'])) {
262 throw new Exception('unable to get syslog data');
263 }
264
265 return explode("\n", $data['body']);
266 }
267
c9e082da
S
268 /*
269 // we cant encrypt and decrypt AES with mode CCM, we need AES with CCM mode for the commands
270 // (stupid, all other data are send as plaintext and some 'normal' data are encrypted...)
271 public function reconnectLte () {
272 $path = 'data/modules.json';
273 $fields = array('csrf_token' => 'nulltoken', 'showpw' => 0, 'password' => $this->hash, 'lte_reconn' => 'true');
274 $cookie = 'challengev='.$this->challenge.'; '.$this->session;
275 $data = $this->sentRequest($path, $fields, $cookie);
276 $json = json_decode($data['body'], true);
277
278 return $json;
279 }
280 */
9bd25bb4
S
281 /*
282 public function resetToFactoryDefault () {
283 $path = 'data/resetAllSetting.json';
284 $fields = array('csrf_token' => 'nulltoken', 'showpw' => 0, 'password' => $this->hash, 'reset_all' => 'true');
285 $cookie = 'challengev='.$this->challenge.'; '.$this->session;
286 $data = $this->sentRequest($path, $fields, $cookie);
287 $json = json_decode($data['body'], true);
288
289 return $json;
290 }
291 */
292
4ae464d1
S
293 /**
294 * check if firmware is actual
295 *
296 * @return array
297 */
298 public function checkFirmware () {
299 $path = 'data/checkfirmware.json';
300 $fields = array('checkfirmware' => 'true');
301 $cookie = 'challengev='.$this->challenge.'; '.$this->session;
302 $data = $this->sentRequest($path, $fields, $cookie);
303
304 if (empty($data['body'])) {
305 throw new Exception('unable to get checkfirmware data');
306 }
307
308 $json = json_decode($data['body'], true);
309
310 return $json;
311 }
312
14d4f286
S
313 /**
314 * decrypt data from router
315 *
316 * @param string $data
317 * @return array
318 */
319 public function decrypt ($data) {
320 require_once 'CryptLib/CryptLib.php';
321 $factory = new CryptLib\Cipher\Factory();
322 $aes = $factory->getBlockCipher('rijndael-128');
323
324 $iv = hex2bin(substr($this->challenge, 16, 16));
325 $adata = hex2bin(substr($this->challenge, 32, 16));
326 $dkey = hex2bin($this->derivedk);
327 $enc = hex2bin($data);
328
329 $aes->setKey($dkey);
330 $mode = $factory->getMode('ccm', $aes, $iv, [ 'adata' => $adata, 'lSize' => 7]);
331
332 $mode->decrypt($enc);
333
334 return $mode->finish();
335 }
336
337 /**
338 * decrypt data for the router
339 *
340 * @param array $data
341 * @return string
342 */
343 public function encrypt ($data) {
344 require_once 'CryptLib/CryptLib.php';
345 $factory = new CryptLib\Cipher\Factory();
346 $aes = $factory->getBlockCipher('rijndael-128');
347
348 $iv = hex2bin(substr($this->challenge, 16, 16));
349 $adata = hex2bin(substr($this->challenge, 32, 16));
350 $dkey = hex2bin($this->derivedk);
351
352 $aes->setKey($dkey);
353 $mode = $factory->getMode('ccm', $aes, $iv, [ 'adata' => $adata, 'lSize' => 7]);
354 $mode->encrypt(http_build_query($data));
355
356 return $mode->finish();
357 }
358
a91317a6
S
359 /**
360 * sends the request to router
5e44cffa 361 *
b5a532a8 362 * @param string $path
a91317a6
S
363 * @param array $fields
364 * @param string $cookie
365 * @return array
366 */
b5a532a8
S
367 private function sentRequest ($path, $fields = array(), $cookie = '') {
368 $url = $this->url.$path;
a91317a6
S
369 $ch = curl_init();
370 curl_setopt($ch, CURLOPT_URL, $url);
371
372 if (!empty($fields)) {
373 curl_setopt($ch, CURLOPT_POST, count($fields));
374 curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($fields));
375 }
376
377 if (!empty($cookie)) {
378 curl_setopt($ch, CURLOPT_COOKIE, $cookie);
379 }
380
381 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
382 curl_setopt($ch, CURLOPT_HEADER, true);
383
384
385 if ($cookie) {
386
387 }
388
389 $result = curl_exec($ch);
390
391 $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
392 $header = substr($result, 0, $header_size);
393 $body = substr($result, $header_size);
394 curl_close($ch);
395
396 // fix invalid json
14d4f286 397
a91317a6
S
398 $body = preg_replace("/(\r\n)|(\r)/", "\n", $body);
399 $body = preg_replace('/\'/i', '"', $body);
5e44cffa 400 $body = preg_replace("/\[\s+\]/i", '[ {} ]', $body);
dae16c50 401 $body = preg_replace("/},\s+]/", "}\n]", $body);
a91317a6
S
402
403 return array('header' => $this->parse_headers($header), 'body' => $body);
404 }
405
406 /**
407 * parse the curl return header into an array
5e44cffa 408 *
a91317a6
S
409 * @param string $response
410 * @return array
411 */
412 private function parse_headers($response) {
413 $headers = array();
414 $header_text = substr($response, 0, strpos($response, "\r\n\r\n"));
415
416 foreach (explode("\r\n", $header_text) as $i => $line) {
417 if ($i === 0) {
418 $headers['http_code'] = $line;
419 }
420 else {
421 list ($key, $value) = explode(': ', $line);
422 $headers[$key] = $value;
423 }
424 }
425
426 return $headers;
427 }
428}