homebridge-melcloud-control 4.4.1-beta.25 → 4.4.1-beta.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/helper.js +70 -0
- package/src/melcloudhome.js +59 -71
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"displayName": "MELCloud Control",
|
|
3
3
|
"name": "homebridge-melcloud-control",
|
|
4
|
-
"version": "4.4.1-beta.
|
|
4
|
+
"version": "4.4.1-beta.27",
|
|
5
5
|
"description": "Homebridge plugin to control Mitsubishi Air Conditioner, Heat Pump and Energy Recovery Ventilation.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "grzegorz914",
|
package/src/helper.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
class CookieJar {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.cookies = [];
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
addFromHeader(setCookie, url) {
|
|
7
|
+
if (!setCookie) return;
|
|
8
|
+
|
|
9
|
+
const urlObj = new URL(url);
|
|
10
|
+
const headers = Array.isArray(setCookie) ? setCookie : [setCookie];
|
|
11
|
+
|
|
12
|
+
for (const header of headers) {
|
|
13
|
+
const parts = header.split(';').map(p => p.trim());
|
|
14
|
+
const [name, value] = parts[0].split('=');
|
|
15
|
+
|
|
16
|
+
const cookie = {
|
|
17
|
+
name,
|
|
18
|
+
value,
|
|
19
|
+
domain: urlObj.hostname,
|
|
20
|
+
path: '/',
|
|
21
|
+
expires: null,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
for (const part of parts.slice(1)) {
|
|
25
|
+
const [key, val] = part.split('=').map(v => v?.trim());
|
|
26
|
+
if (!key) continue;
|
|
27
|
+
|
|
28
|
+
switch (key.toLowerCase()) {
|
|
29
|
+
case 'domain':
|
|
30
|
+
cookie.domain = val;
|
|
31
|
+
break;
|
|
32
|
+
case 'path':
|
|
33
|
+
cookie.path = val || '/';
|
|
34
|
+
break;
|
|
35
|
+
case 'expires':
|
|
36
|
+
cookie.expires = new Date(val);
|
|
37
|
+
break;
|
|
38
|
+
case 'max-age':
|
|
39
|
+
cookie.expires = new Date(Date.now() + Number(val) * 1000);
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.cookies = this.cookies.filter(
|
|
45
|
+
c => !(c.name === cookie.name && c.domain === cookie.domain && c.path === cookie.path),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
this.cookies.push(cookie);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
headerFor(url) {
|
|
53
|
+
const urlObj = new URL(url);
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
|
|
56
|
+
const validCookies = this.cookies.filter(cookie => {
|
|
57
|
+
if (cookie.expires && cookie.expires.getTime() <= now) return false;
|
|
58
|
+
if (!urlObj.hostname.endsWith(cookie.domain)) return false;
|
|
59
|
+
if (!urlObj.pathname.startsWith(cookie.path)) return false;
|
|
60
|
+
return true;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return validCookies.length
|
|
64
|
+
? validCookies.map(c => `${c.name}=${c.value}`).join('; ')
|
|
65
|
+
: null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
export default CookieJar
|
package/src/melcloudhome.js
CHANGED
|
@@ -9,6 +9,8 @@ import Functions from './functions.js';
|
|
|
9
9
|
import { ApiUrls, LanguageLocaleMap } from './constants.js';
|
|
10
10
|
const execPromise = promisify(exec);
|
|
11
11
|
|
|
12
|
+
import CookieJar from './helper.js';
|
|
13
|
+
|
|
12
14
|
import crypto from 'crypto';
|
|
13
15
|
import { URL } from 'url';
|
|
14
16
|
|
|
@@ -31,8 +33,6 @@ class MelCloudHome extends EventEmitter {
|
|
|
31
33
|
this.socketConnected = false;
|
|
32
34
|
this.heartbeat = null;
|
|
33
35
|
|
|
34
|
-
this.cookies = [];
|
|
35
|
-
|
|
36
36
|
this.functions = new Functions(this.logWarn, this.logError, this.logDebug)
|
|
37
37
|
.on('warn', warn => this.emit('warn', warn))
|
|
38
38
|
.on('error', error => this.emit('error', error))
|
|
@@ -447,53 +447,93 @@ class MelCloudHome extends EventEmitter {
|
|
|
447
447
|
}
|
|
448
448
|
|
|
449
449
|
|
|
450
|
-
async axiosRequest(url, options, jar, maxRedirects =
|
|
450
|
+
async axiosRequest(url, options, jar, maxRedirects = 15) {
|
|
451
451
|
let currentUrl = url;
|
|
452
|
+
let previousUrl = null;
|
|
453
|
+
let method = options.method || 'GET';
|
|
454
|
+
let body = options.body;
|
|
452
455
|
|
|
453
456
|
for (let i = 0; i < maxRedirects; i++) {
|
|
457
|
+
const cookieHeader = jar.headerFor(currentUrl);
|
|
458
|
+
|
|
459
|
+
const headers = {
|
|
460
|
+
'User-Agent': options.headers?.['User-Agent'],
|
|
461
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
462
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
463
|
+
'Accept-Encoding': 'identity',
|
|
464
|
+
...(previousUrl ? { Referer: previousUrl } : {}),
|
|
465
|
+
...(cookieHeader ? { Cookie: cookieHeader } : {}),
|
|
466
|
+
...options.headers,
|
|
467
|
+
};
|
|
468
|
+
|
|
454
469
|
const response = await axios({
|
|
455
470
|
url: currentUrl,
|
|
456
|
-
method
|
|
457
|
-
data:
|
|
458
|
-
headers
|
|
459
|
-
...options.headers,
|
|
460
|
-
...(jar.headerFor(currentUrl) && { Cookie: jar.headerFor(currentUrl) }),
|
|
461
|
-
},
|
|
462
|
-
validateStatus: () => true,
|
|
471
|
+
method,
|
|
472
|
+
data: body,
|
|
473
|
+
headers,
|
|
463
474
|
maxRedirects: 0,
|
|
475
|
+
validateStatus: () => true,
|
|
464
476
|
responseType: 'text',
|
|
465
477
|
});
|
|
466
478
|
|
|
467
479
|
jar.addFromHeader(response.headers['set-cookie'], currentUrl);
|
|
468
480
|
|
|
481
|
+
/* ========= HTTP REDIRECT ========= */
|
|
469
482
|
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
470
483
|
const location = response.headers.location;
|
|
471
484
|
if (!location) break;
|
|
472
485
|
|
|
486
|
+
// SUCCESS: app redirect
|
|
473
487
|
if (location.startsWith('melcloudhome://')) {
|
|
474
488
|
return { url: location, data: response.data };
|
|
475
489
|
}
|
|
476
490
|
|
|
491
|
+
previousUrl = currentUrl;
|
|
477
492
|
currentUrl = location.startsWith('http')
|
|
478
493
|
? location
|
|
479
494
|
: new URL(location, currentUrl).toString();
|
|
480
495
|
|
|
481
|
-
|
|
482
|
-
|
|
496
|
+
method = 'GET';
|
|
497
|
+
body = undefined;
|
|
483
498
|
continue;
|
|
484
499
|
}
|
|
485
500
|
|
|
486
|
-
|
|
501
|
+
/* ========= META REFRESH ========= */
|
|
502
|
+
const metaMatch = response.data?.match(
|
|
503
|
+
/<meta[^>]+http-equiv=["']?refresh["']?[^>]+content=["']?\d+;\s*url=([^"'>]+)["']?/i
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
if (metaMatch) {
|
|
507
|
+
const redirectPath = metaMatch[1]
|
|
508
|
+
.replace(/&/g, '&')
|
|
509
|
+
.replace(///g, '/');
|
|
510
|
+
|
|
511
|
+
previousUrl = currentUrl;
|
|
512
|
+
currentUrl = redirectPath.startsWith('http')
|
|
513
|
+
? redirectPath
|
|
514
|
+
: new URL(redirectPath, currentUrl).toString();
|
|
515
|
+
|
|
516
|
+
method = 'GET';
|
|
517
|
+
body = undefined;
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/* ========= FINAL RESPONSE ========= */
|
|
522
|
+
return {
|
|
523
|
+
url: currentUrl,
|
|
524
|
+
data: response.data,
|
|
525
|
+
headers: response.headers,
|
|
526
|
+
};
|
|
487
527
|
}
|
|
488
528
|
|
|
489
|
-
throw new Error('
|
|
529
|
+
throw new Error('OAuth flow aborted: too many redirects');
|
|
490
530
|
}
|
|
491
531
|
|
|
532
|
+
|
|
492
533
|
async loginWithCredentials(email, password) {
|
|
493
534
|
const jar = new CookieJar();
|
|
494
535
|
|
|
495
|
-
const userAgent =
|
|
496
|
-
'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.1 Mobile Safari/604.1';
|
|
536
|
+
const userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.1 Mobile Safari/604.1';
|
|
497
537
|
|
|
498
538
|
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
|
499
539
|
const codeChallenge = crypto
|
|
@@ -501,8 +541,7 @@ class MelCloudHome extends EventEmitter {
|
|
|
501
541
|
.update(codeVerifier)
|
|
502
542
|
.digest('base64url');
|
|
503
543
|
|
|
504
|
-
const authUrl =
|
|
505
|
-
'https://auth.melcloudhome.com/connect/authorize?' +
|
|
544
|
+
const authUrl = 'https://auth.melcloudhome.com/connect/authorize?' +
|
|
506
545
|
new URLSearchParams({
|
|
507
546
|
client_id: 'homemobile',
|
|
508
547
|
redirect_uri: 'melcloudhome://',
|
|
@@ -517,9 +556,7 @@ class MelCloudHome extends EventEmitter {
|
|
|
517
556
|
headers: { 'User-Agent': userAgent },
|
|
518
557
|
}, jar);
|
|
519
558
|
|
|
520
|
-
const csrfMatch =
|
|
521
|
-
loginPage.data.match(/name="(_csrf|__RequestVerificationToken)".*?value="([^"]+)"/);
|
|
522
|
-
|
|
559
|
+
const csrfMatch = loginPage.data.match(/name="(_csrf|__RequestVerificationToken)".*?value="([^"]+)"/);
|
|
523
560
|
if (!csrfMatch) throw new Error('CSRF token not found');
|
|
524
561
|
const csrfToken = csrfMatch[2];
|
|
525
562
|
|
|
@@ -548,8 +585,7 @@ class MelCloudHome extends EventEmitter {
|
|
|
548
585
|
if (!code) throw new Error('Authorization code missing');
|
|
549
586
|
|
|
550
587
|
// STEP 3 – exchange token
|
|
551
|
-
const tokenResponse = await axios.post(
|
|
552
|
-
'https://auth.melcloudhome.com/connect/token',
|
|
588
|
+
const tokenResponse = await axios.post('https://auth.melcloudhome.com/connect/token',
|
|
553
589
|
new URLSearchParams({
|
|
554
590
|
grant_type: 'authorization_code',
|
|
555
591
|
client_id: 'homemobile',
|
|
@@ -576,54 +612,6 @@ class MelCloudHome extends EventEmitter {
|
|
|
576
612
|
};
|
|
577
613
|
}
|
|
578
614
|
|
|
579
|
-
addFromHeader(setCookie, url) {
|
|
580
|
-
if (!setCookie) return;
|
|
581
|
-
const urlObj = new URL(url);
|
|
582
|
-
const cookies = Array.isArray(setCookie) ? setCookie : [setCookie];
|
|
583
|
-
|
|
584
|
-
for (const header of cookies) {
|
|
585
|
-
const parts = header.split(';').map(p => p.trim());
|
|
586
|
-
const [name, value] = parts[0].split('=');
|
|
587
|
-
|
|
588
|
-
const cookie = {
|
|
589
|
-
name,
|
|
590
|
-
value,
|
|
591
|
-
domain: urlObj.hostname,
|
|
592
|
-
path: '/',
|
|
593
|
-
expires: null,
|
|
594
|
-
};
|
|
595
|
-
|
|
596
|
-
for (const part of parts.slice(1)) {
|
|
597
|
-
const [k, v] = part.split('=');
|
|
598
|
-
if (!k) continue;
|
|
599
|
-
const key = k.toLowerCase();
|
|
600
|
-
if (key === 'domain') cookie.domain = v;
|
|
601
|
-
if (key === 'path') cookie.path = v;
|
|
602
|
-
if (key === 'expires') cookie.expires = new Date(v);
|
|
603
|
-
if (key === 'max-age') cookie.expires = new Date(Date.now() + Number(v) * 1000);
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
this.cookies = this.cookies.filter(
|
|
607
|
-
c => !(c.name === cookie.name && c.domain === cookie.domain && c.path === cookie.path),
|
|
608
|
-
);
|
|
609
|
-
this.cookies.push(cookie);
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
headerFor(url) {
|
|
614
|
-
const urlObj = new URL(url);
|
|
615
|
-
const now = Date.now();
|
|
616
|
-
|
|
617
|
-
return this.cookies
|
|
618
|
-
.filter(c =>
|
|
619
|
-
(!c.expires || c.expires.getTime() > now) &&
|
|
620
|
-
urlObj.hostname.endsWith(c.domain) &&
|
|
621
|
-
urlObj.pathname.startsWith(c.path),
|
|
622
|
-
)
|
|
623
|
-
.map(c => `${c.name}=${c.value}`)
|
|
624
|
-
.join('; ');
|
|
625
|
-
}
|
|
626
|
-
|
|
627
615
|
async connect() {
|
|
628
616
|
if (this.logDebug) this.emit('debug', 'Connecting to MELCloud Home');
|
|
629
617
|
|