homebridge-melcloud-control 4.3.11-beta.20 → 4.3.11-beta.21
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 +71 -187
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.21",
|
|
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,112 @@
|
|
|
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
|
+
'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';
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"https://live-melcloudhome.auth.eu-west-1.amazoncognito.com";
|
|
13
|
-
|
|
14
|
-
class MELCloudHomeAuth {
|
|
15
|
-
constructor({ debug = false } = {}) {
|
|
16
|
-
this.debug = debug;
|
|
11
|
+
export class MELCloudHomeAuth {
|
|
12
|
+
constructor() {
|
|
17
13
|
this.jar = new CookieJar();
|
|
18
|
-
this.
|
|
14
|
+
this.client = wrapper(
|
|
19
15
|
axios.create({
|
|
20
16
|
jar: this.jar,
|
|
21
17
|
withCredentials: true,
|
|
22
|
-
maxRedirects: 10,
|
|
23
18
|
headers: {
|
|
24
|
-
|
|
19
|
+
'User-Agent': USER_AGENT,
|
|
25
20
|
Accept:
|
|
26
|
-
|
|
27
|
-
|
|
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',
|
|
28
23
|
},
|
|
24
|
+
maxRedirects: 0,
|
|
25
|
+
validateStatus: (status) => status >= 200 && status < 400,
|
|
29
26
|
})
|
|
30
27
|
);
|
|
31
28
|
this.authenticated = false;
|
|
32
29
|
}
|
|
33
30
|
|
|
34
|
-
|
|
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;
|
|
31
|
+
async login(username, password) {
|
|
63
32
|
try {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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();
|
|
90
|
-
}
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async startLoginFlow() {
|
|
95
|
-
this.log("Starting login flow: GET /bff/login");
|
|
96
|
-
const resp = await this.axios.get(`${BASE_URL}/bff/login`, {
|
|
97
|
-
params: { returnUrl: "/dashboard" },
|
|
98
|
-
validateStatus: () => true,
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
const finalUrl =
|
|
102
|
-
resp.request?.res?.responseUrl || resp.config?.url || "";
|
|
103
|
-
|
|
104
|
-
this.log("Received final redirect URL:", finalUrl);
|
|
105
|
-
|
|
106
|
-
const csrf = this.extractCsrf(resp.data);
|
|
107
|
-
if (!csrf) throw new Error("Failed to extract CSRF token");
|
|
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
|
+
});
|
|
108
37
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
38
|
+
// Follow redirect to Cognito
|
|
39
|
+
let finalUrl = resp.request.res.responseUrl;
|
|
40
|
+
if (!finalUrl.includes('.amazoncognito.com')) {
|
|
41
|
+
throw new Error('Unexpected redirect URL: ' + finalUrl);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Extract CSRF token from HTML
|
|
45
|
+
const csrfToken = this.extractCsrfToken(resp.data);
|
|
46
|
+
if (!csrfToken) throw new Error('CSRF token not found');
|
|
47
|
+
|
|
48
|
+
// 2. POST credentials to Cognito
|
|
49
|
+
const loginData = qs.stringify({
|
|
50
|
+
_csrf: csrfToken,
|
|
51
|
+
username,
|
|
52
|
+
password,
|
|
53
|
+
cognitoAsfData: '', // minimal value, może być potrzebne pełne ASF
|
|
54
|
+
});
|
|
112
55
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
// Step 1: POST credentials
|
|
124
|
-
const resp = await this.axios.post(loginUrl, payload.toString(), {
|
|
125
|
-
headers: {
|
|
126
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
127
|
-
Origin: COGNITO_BASE,
|
|
128
|
-
Referer: loginUrl,
|
|
129
|
-
},
|
|
130
|
-
validateStatus: () => true,
|
|
131
|
-
maxRedirects: 0,
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
const redirectUrl = resp.headers.location || resp.request?.res?.responseUrl;
|
|
135
|
-
if (!redirectUrl.includes("signin-oidc")) {
|
|
136
|
-
throw new Error(
|
|
137
|
-
"Authentication failed: signin-oidc callback not received"
|
|
138
|
-
);
|
|
139
|
-
}
|
|
56
|
+
resp = await this.client.post(finalUrl, loginData, {
|
|
57
|
+
headers: {
|
|
58
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
59
|
+
Origin: 'https://live-melcloudhome.auth.eu-west-1.amazoncognito.com',
|
|
60
|
+
Referer: finalUrl,
|
|
61
|
+
},
|
|
62
|
+
maxRedirects: 5,
|
|
63
|
+
});
|
|
140
64
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
);
|
|
65
|
+
finalUrl = resp.request.res.responseUrl;
|
|
66
|
+
|
|
67
|
+
// Sprawdzenie czy udało się zalogować
|
|
68
|
+
if (
|
|
69
|
+
finalUrl.includes('melcloudhome.com/dashboard') &&
|
|
70
|
+
resp.status < 400
|
|
71
|
+
) {
|
|
72
|
+
console.log('Authentication successful');
|
|
73
|
+
this.authenticated = true;
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
throw new Error('Authentication failed, final URL: ' + finalUrl);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
throw new Error('Login error: ' + err.message);
|
|
162
80
|
}
|
|
163
|
-
|
|
164
|
-
this.authenticated = true;
|
|
165
|
-
this.log("Login successful, session cookie meu.identity is present");
|
|
166
|
-
await this._delay(1000); // short wait for Blazor initialization
|
|
167
|
-
return true;
|
|
168
81
|
}
|
|
169
82
|
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const valid = await this.checkSession();
|
|
175
|
-
if (!valid) throw new Error("Session not valid after login");
|
|
176
|
-
|
|
177
|
-
this.log("Login flow complete");
|
|
178
|
-
return true;
|
|
83
|
+
extractCsrfToken(html) {
|
|
84
|
+
const dom = new JSDOM(html);
|
|
85
|
+
const input = dom.window.document.querySelector('input[name="_csrf"]');
|
|
86
|
+
return input?.value || null;
|
|
179
87
|
}
|
|
180
88
|
|
|
181
89
|
async checkSession() {
|
|
182
90
|
if (!this.authenticated) return false;
|
|
91
|
+
|
|
183
92
|
try {
|
|
184
|
-
const resp = await this.
|
|
185
|
-
headers: {
|
|
186
|
-
Accept: "application/json",
|
|
187
|
-
"x-csrf": "1",
|
|
188
|
-
Referer: `${BASE_URL}/dashboard`,
|
|
189
|
-
},
|
|
190
|
-
validateStatus: () => true,
|
|
93
|
+
const resp = await this.client.get(`${BASE_URL}/api/user/context`, {
|
|
94
|
+
headers: { 'x-csrf': '1', Referer: `${BASE_URL}/dashboard` },
|
|
191
95
|
});
|
|
192
96
|
return resp.status === 200;
|
|
193
97
|
} catch {
|
|
98
|
+
this.authenticated = false;
|
|
194
99
|
return false;
|
|
195
100
|
}
|
|
196
101
|
}
|
|
197
102
|
|
|
198
|
-
|
|
199
|
-
|
|
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;
|
|
221
|
-
}
|
|
103
|
+
getCookies() {
|
|
104
|
+
return this.jar.toJSON();
|
|
222
105
|
}
|
|
223
106
|
}
|
|
224
107
|
|
|
225
108
|
|
|
109
|
+
|
|
226
110
|
export default MELCloudHomeAuth;
|
|
227
111
|
|
|
228
112
|
|