homebridge-melcloud-control 4.3.11-beta.20 → 4.3.11-beta.22
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/melcloudhome.js +7 -14
- package/src/melcloudhomeauth.js +98 -213
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"displayName": "MELCloud Control",
|
|
3
3
|
"name": "homebridge-melcloud-control",
|
|
4
|
-
"version": "4.3.11-beta.
|
|
4
|
+
"version": "4.3.11-beta.22",
|
|
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/melcloudhome.js
CHANGED
|
@@ -457,24 +457,17 @@ class MelCloudHome extends EventEmitter {
|
|
|
457
457
|
|
|
458
458
|
async connect() {
|
|
459
459
|
if (this.logDebug) this.emit('debug', 'Connecting to MELCloud Home');
|
|
460
|
-
const
|
|
460
|
+
const auth = new MELCloudHomeAuth();
|
|
461
461
|
try {
|
|
462
|
-
await
|
|
463
|
-
console.log(
|
|
462
|
+
await auth.login(this.user, this.passwd);
|
|
463
|
+
console.log('Logged in!');
|
|
464
464
|
|
|
465
|
-
const
|
|
466
|
-
console.log(
|
|
465
|
+
const valid = await auth.checkSession();
|
|
466
|
+
console.log('Session valid:', valid);
|
|
467
467
|
|
|
468
|
-
|
|
469
|
-
console.log("Devices:", JSON.stringify(devices, null, 2));
|
|
470
|
-
|
|
471
|
-
// If you need a token-like object:
|
|
472
|
-
const tokens = await client.getAuthTokenCandidates();
|
|
473
|
-
console.log("Token candidates:", tokens);
|
|
468
|
+
console.log('Cookies:', auth.getCookies());
|
|
474
469
|
} catch (err) {
|
|
475
|
-
console.error(
|
|
476
|
-
} finally {
|
|
477
|
-
await client.logout();
|
|
470
|
+
console.error(err.message);
|
|
478
471
|
}
|
|
479
472
|
}
|
|
480
473
|
}
|
package/src/melcloudhomeauth.js
CHANGED
|
@@ -1,228 +1,113 @@
|
|
|
1
|
-
import axios from
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { JSDOM } from
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { CookieJar } from 'tough-cookie';
|
|
3
|
+
import { wrapper } from 'axios-cookiejar-support';
|
|
4
|
+
import { JSDOM } from 'jsdom';
|
|
5
|
+
import qs from 'qs';
|
|
5
6
|
|
|
6
|
-
//
|
|
7
|
+
const BASE_URL = 'https://melcloudhome.com'; // lub właściwe dla Home
|
|
7
8
|
const USER_AGENT =
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
},
|
|
29
|
-
})
|
|
30
|
-
);
|
|
31
|
-
this.authenticated = false;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
log(...args) {
|
|
35
|
-
if (this.debug) console.log("[MelcloudHomeClient]", ...args);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
_delay(ms) {
|
|
39
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// --- Helper: generate cognitoAsfData (simplified) ---
|
|
43
|
-
generateAsfData() {
|
|
44
|
-
const data = {
|
|
45
|
-
deviceName: "Chrome",
|
|
46
|
-
osName: "Macintosh",
|
|
47
|
-
osVersion: "14_5",
|
|
48
|
-
timezoneOffset: new Date().getTimezoneOffset(),
|
|
49
|
-
userAgent: USER_AGENT,
|
|
50
|
-
screen: { width: 1440, height: 900 },
|
|
51
|
-
language: "en-US",
|
|
52
|
-
plugins: [],
|
|
53
|
-
fonts: [],
|
|
54
|
-
canvasFingerprint: "simplified", // placeholder
|
|
55
|
-
};
|
|
56
|
-
const json = JSON.stringify(data);
|
|
57
|
-
return Buffer.from(json).toString("base64");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// --- Extract CSRF token from HTML ---
|
|
61
|
-
extractCsrf(html) {
|
|
62
|
-
if (!html) return null;
|
|
63
|
-
try {
|
|
64
|
-
const dom = new JSDOM(html);
|
|
65
|
-
const el = dom.window.document.querySelector('input[name="_csrf"]');
|
|
66
|
-
if (el && el.value) return el.value;
|
|
67
|
-
} catch {}
|
|
68
|
-
const m = html.match(/<input[^>]+name="_csrf"[^>]+value="([^"]+)"/i);
|
|
69
|
-
if (m) return m[1];
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
findCognitoLoginUrl(html) {
|
|
74
|
-
if (!html) return null;
|
|
75
|
-
const m = html.match(/action="([^"]*amazoncognito\.com[^"]*)"/i);
|
|
76
|
-
if (m) return m[1];
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
extractErrorMessage(html) {
|
|
81
|
-
if (!html) return null;
|
|
82
|
-
const patterns = [
|
|
83
|
-
/<div[^>]*class="[^"]*error[^"]*"[^>]*>([^<]+)<\/div>/i,
|
|
84
|
-
/<span[^>]*class="[^"]*error[^"]*"[^>]*>([^<]+)<\/span>/i,
|
|
85
|
-
/<p[^>]*class="[^"]*error[^"]*"[^>]*>([^<]+)<\/p>/i,
|
|
86
|
-
];
|
|
87
|
-
for (const p of patterns) {
|
|
88
|
-
const m = html.match(p);
|
|
89
|
-
if (m) return m[1].trim();
|
|
9
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36';
|
|
10
|
+
|
|
11
|
+
export class MELCloudHomeAuth {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.jar = new CookieJar();
|
|
14
|
+
this.client = wrapper(
|
|
15
|
+
axios.create({
|
|
16
|
+
jar: this.jar,
|
|
17
|
+
withCredentials: true,
|
|
18
|
+
headers: {
|
|
19
|
+
'User-Agent': USER_AGENT,
|
|
20
|
+
Accept:
|
|
21
|
+
'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
|
|
22
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
23
|
+
},
|
|
24
|
+
maxRedirects: 0,
|
|
25
|
+
validateStatus: (status) => status >= 200 && status < 400,
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
this.authenticated = false;
|
|
90
29
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
30
|
+
|
|
31
|
+
async login(username, password) {
|
|
32
|
+
try {
|
|
33
|
+
// 1. GET login page to get CSRF token
|
|
34
|
+
let resp = await this.client.get(`${BASE_URL}/bff/login`, {
|
|
35
|
+
params: { returnUrl: '/dashboard' },
|
|
36
|
+
maxRedirects: 5, // pozwala axios śledzić redirecty
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// finalny URL po redirectach
|
|
40
|
+
let finalUrl = resp.request?.res?.responseUrl || resp.request?.path || '';
|
|
41
|
+
if (!finalUrl.includes('.amazoncognito.com')) {
|
|
42
|
+
throw new Error('Unexpected redirect URL: ' + finalUrl);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Extract CSRF token from HTML
|
|
46
|
+
const csrfToken = this.extractCsrfToken(resp.data);
|
|
47
|
+
if (!csrfToken) throw new Error('CSRF token not found');
|
|
48
|
+
|
|
49
|
+
// 2. POST credentials to Cognito
|
|
50
|
+
const loginData = qs.stringify({
|
|
51
|
+
_csrf: csrfToken,
|
|
52
|
+
username,
|
|
53
|
+
password,
|
|
54
|
+
cognitoAsfData: '', // minimal value, może być potrzebne pełne ASF
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
resp = await this.client.post(finalUrl, loginData, {
|
|
58
|
+
headers: {
|
|
59
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
60
|
+
Origin: 'https://live-melcloudhome.auth.eu-west-1.amazoncognito.com',
|
|
61
|
+
Referer: finalUrl,
|
|
62
|
+
},
|
|
63
|
+
maxRedirects: 5,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
finalUrl = resp.request.res.responseUrl;
|
|
67
|
+
|
|
68
|
+
// Sprawdzenie czy udało się zalogować
|
|
69
|
+
if (
|
|
70
|
+
finalUrl.includes('melcloudhome.com/dashboard') &&
|
|
71
|
+
resp.status < 400
|
|
72
|
+
) {
|
|
73
|
+
console.log('Authentication successful');
|
|
74
|
+
this.authenticated = true;
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
throw new Error('Authentication failed, final URL: ' + finalUrl);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
throw new Error('Login error: ' + err.message);
|
|
81
|
+
}
|
|
139
82
|
}
|
|
140
83
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
headers: {
|
|
146
|
-
Referer: loginUrl,
|
|
147
|
-
"User-Agent": USER_AGENT,
|
|
148
|
-
Accept: "text/html",
|
|
149
|
-
},
|
|
150
|
-
validateStatus: () => true,
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
this.log("signin-oidc status:", callback.status);
|
|
154
|
-
|
|
155
|
-
// Check if cookie meu.identity is set
|
|
156
|
-
const cookies = await this.jar.getCookies(BASE_URL);
|
|
157
|
-
const hasIdentity = cookies.some((c) => c.key.includes("meu.identity"));
|
|
158
|
-
if (!hasIdentity) {
|
|
159
|
-
throw new Error(
|
|
160
|
-
"OpenID callback did not produce session cookie (cognitoAsfData missing or rejected)"
|
|
161
|
-
);
|
|
84
|
+
extractCsrfToken(html) {
|
|
85
|
+
const dom = new JSDOM(html);
|
|
86
|
+
const input = dom.window.document.querySelector('input[name="_csrf"]');
|
|
87
|
+
return input?.value || null;
|
|
162
88
|
}
|
|
163
89
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
this.log("Login flow complete");
|
|
178
|
-
return true;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async checkSession() {
|
|
182
|
-
if (!this.authenticated) return false;
|
|
183
|
-
try {
|
|
184
|
-
const resp = await this.axios.get(`${BASE_URL}/api/user/context`, {
|
|
185
|
-
headers: {
|
|
186
|
-
Accept: "application/json",
|
|
187
|
-
"x-csrf": "1",
|
|
188
|
-
Referer: `${BASE_URL}/dashboard`,
|
|
189
|
-
},
|
|
190
|
-
validateStatus: () => true,
|
|
191
|
-
});
|
|
192
|
-
return resp.status === 200;
|
|
193
|
-
} catch {
|
|
194
|
-
return false;
|
|
90
|
+
async checkSession() {
|
|
91
|
+
if (!this.authenticated) return false;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const resp = await this.client.get(`${BASE_URL}/api/user/context`, {
|
|
95
|
+
headers: { 'x-csrf': '1', Referer: `${BASE_URL}/dashboard` },
|
|
96
|
+
});
|
|
97
|
+
return resp.status === 200;
|
|
98
|
+
} catch {
|
|
99
|
+
this.authenticated = false;
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
195
102
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (!this.authenticated) throw new Error("Not authenticated");
|
|
200
|
-
const resp = await this.axios.get(`${BASE_URL}/api/user/context`, {
|
|
201
|
-
headers: { Accept: "application/json", "x-csrf": "1", Referer: `${BASE_URL}/dashboard` },
|
|
202
|
-
});
|
|
203
|
-
return resp.data;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
async getDevices() {
|
|
207
|
-
if (!this.authenticated) throw new Error("Not authenticated");
|
|
208
|
-
const resp = await this.axios.get(`${BASE_URL}/api/devices/devices`, {
|
|
209
|
-
headers: { Accept: "application/json", "x-csrf": "1", Referer: `${BASE_URL}/dashboard` },
|
|
210
|
-
validateStatus: () => true,
|
|
211
|
-
});
|
|
212
|
-
if (resp.status !== 200) throw new Error(`Failed to get devices: ${resp.status}`);
|
|
213
|
-
return resp.data;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
async logout() {
|
|
217
|
-
try {
|
|
218
|
-
await this.axios.get(`${BASE_URL}/bff/logout`, { validateStatus: () => true });
|
|
219
|
-
} finally {
|
|
220
|
-
this.authenticated = false;
|
|
103
|
+
|
|
104
|
+
getCookies() {
|
|
105
|
+
return this.jar.toJSON();
|
|
221
106
|
}
|
|
222
|
-
}
|
|
223
107
|
}
|
|
224
108
|
|
|
225
109
|
|
|
110
|
+
|
|
226
111
|
export default MELCloudHomeAuth;
|
|
227
112
|
|
|
228
113
|
|