homebridge-melcloud-control 4.0.0-beta.391 → 4.0.0-beta.392
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/melcloudhometoken.js +88 -126
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"displayName": "MELCloud Control",
|
|
3
3
|
"name": "homebridge-melcloud-control",
|
|
4
|
-
"version": "4.0.0-beta.
|
|
4
|
+
"version": "4.0.0-beta.392",
|
|
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/melcloudhometoken.js
CHANGED
|
@@ -35,39 +35,79 @@ class MelCloudHomeToken extends EventEmitter {
|
|
|
35
35
|
return crypto.randomBytes(32).toString('hex');
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
async
|
|
39
|
-
const
|
|
40
|
-
|
|
38
|
+
async buildAuthorizeUrl() {
|
|
39
|
+
const pkce = this.generatePKCE();
|
|
40
|
+
const state = this.generateState();
|
|
41
|
+
|
|
42
|
+
const authUrl = new URL(AUTHORIZE_ENDPOINT);
|
|
43
|
+
authUrl.searchParams.set('client_id', CLIENT_ID);
|
|
44
|
+
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
|
|
45
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
46
|
+
authUrl.searchParams.set('scope', SCOPE);
|
|
47
|
+
authUrl.searchParams.set('code_challenge', pkce.challenge);
|
|
48
|
+
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
49
|
+
authUrl.searchParams.set('state', state);
|
|
50
|
+
|
|
51
|
+
return { url: authUrl.toString(), codeVerifier: pkce.verifier };
|
|
41
52
|
}
|
|
42
53
|
|
|
43
|
-
async
|
|
54
|
+
async loginToMelCloudHome(authUrl) {
|
|
44
55
|
try {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
client_id: CLIENT_ID,
|
|
50
|
-
code_verifier: codeVerifier,
|
|
51
|
-
});
|
|
56
|
+
const getResp = await this.client.get(authUrl, { headers: { 'Accept': 'text/html' } });
|
|
57
|
+
const cookies = getResp.headers['set-cookie'] || [];
|
|
58
|
+
const dom = new JSDOM(getResp.data);
|
|
59
|
+
const csrf = dom.window.document.querySelector('input[name="_csrf"]')?.value;
|
|
52
60
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
if (!csrf) {
|
|
62
|
+
this.emit('warn', 'CSRF token not found');
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
56
65
|
|
|
57
|
-
const
|
|
66
|
+
const formData = new URLSearchParams({
|
|
67
|
+
_csrf: csrf,
|
|
68
|
+
username: this.user,
|
|
69
|
+
password: this.passwd
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const response = await this.client.post(authUrl, formData.toString(), {
|
|
58
73
|
headers: {
|
|
59
|
-
'
|
|
74
|
+
'User-Agent': MOBILE_USER_AGENT,
|
|
75
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
76
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
77
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
78
|
+
'Content-Length': formData.toString().length,
|
|
79
|
+
'Cookie': cookies.join('; '),
|
|
80
|
+
'Origin': 'https://live-melcloudhome.auth.eu-west-1.amazoncognito.com',
|
|
81
|
+
'Referer': authUrl
|
|
60
82
|
},
|
|
83
|
+
maxRedirects: 0,
|
|
84
|
+
validateStatus: status => [200, 302, 400].includes(status)
|
|
61
85
|
});
|
|
62
86
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
} catch (error) {
|
|
67
|
-
if (error.response) {
|
|
68
|
-
this.emit('warn', `Token error response: ${JSON.stringify(error.response.data)}`);
|
|
87
|
+
if (response.status === 400) {
|
|
88
|
+
this.emit('warn', `Login failed: ${response.data}`);
|
|
89
|
+
return null;
|
|
69
90
|
}
|
|
70
|
-
|
|
91
|
+
|
|
92
|
+
// Extract authorization code
|
|
93
|
+
const code = await this.extractCodeFromResponse(
|
|
94
|
+
response.data,
|
|
95
|
+
response.headers,
|
|
96
|
+
async (url) => {
|
|
97
|
+
const r = await this.client.get(url, {
|
|
98
|
+
maxRedirects: 0,
|
|
99
|
+
validateStatus: status => [200, 302, 303].includes(status)
|
|
100
|
+
});
|
|
101
|
+
return this.extractCodeFromResponse(r.data, r.headers, async u => this.extractCodeFromResponse(r.data, r.headers, u));
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (code) this.emit('warn', `Authorization code obtained: ${code}`);
|
|
106
|
+
return code || null;
|
|
107
|
+
|
|
108
|
+
} catch (err) {
|
|
109
|
+
this.emit('warn', `loginToMelCloudHome error: ${err}`);
|
|
110
|
+
return null;
|
|
71
111
|
}
|
|
72
112
|
}
|
|
73
113
|
|
|
@@ -92,7 +132,7 @@ class MelCloudHomeToken extends EventEmitter {
|
|
|
92
132
|
const formActionMatch = data.match(/action="([^"]+)"/);
|
|
93
133
|
|
|
94
134
|
if (formCodeMatch && formStateMatch && formActionMatch) {
|
|
95
|
-
this.emit('warn', 'Found form_post
|
|
135
|
+
this.emit('warn', 'Found form_post, submitting...');
|
|
96
136
|
try {
|
|
97
137
|
const code = await this.submitFormPost(formActionMatch[1], formCodeMatch[1], formStateMatch[1]);
|
|
98
138
|
this.emit('warn', `submitFormPost returned code: ${code}`);
|
|
@@ -105,10 +145,10 @@ class MelCloudHomeToken extends EventEmitter {
|
|
|
105
145
|
}
|
|
106
146
|
}
|
|
107
147
|
|
|
108
|
-
// 3️⃣ JS redirect
|
|
148
|
+
// 3️⃣ JS redirect in body
|
|
109
149
|
const bodyCodeMatch = data.match(/melcloudhome:\/\/[^"'\s]*[?&]code=([^&"'\s]+)/);
|
|
110
150
|
if (bodyCodeMatch) {
|
|
111
|
-
this.emit('warn', `Found code in
|
|
151
|
+
this.emit('warn', `Found code in body: ${bodyCodeMatch[1]}`);
|
|
112
152
|
resolve(bodyCodeMatch[1]);
|
|
113
153
|
return;
|
|
114
154
|
}
|
|
@@ -129,134 +169,55 @@ class MelCloudHomeToken extends EventEmitter {
|
|
|
129
169
|
|
|
130
170
|
this.emit('warn', 'Authorization code not found in response');
|
|
131
171
|
reject(new Error('Authorization code not found'));
|
|
132
|
-
} catch (
|
|
133
|
-
this.emit('warn', `extractCodeFromResponse error: ${
|
|
134
|
-
reject(
|
|
172
|
+
} catch (err) {
|
|
173
|
+
this.emit('warn', `extractCodeFromResponse error: ${err}`);
|
|
174
|
+
reject(err);
|
|
135
175
|
}
|
|
136
176
|
});
|
|
137
177
|
}
|
|
138
178
|
|
|
139
|
-
// submitFormPost w JS
|
|
140
179
|
async submitFormPost(actionUrl, code, state) {
|
|
141
180
|
const formData = new URLSearchParams({ code, state });
|
|
142
181
|
this.emit('warn', `Submitting form_post to ${actionUrl}`);
|
|
143
182
|
const res = await this.client.post(actionUrl, formData.toString(), {
|
|
144
183
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
145
184
|
maxRedirects: 0,
|
|
146
|
-
validateStatus: status => [200, 302, 303].includes(status)
|
|
185
|
+
validateStatus: status => [200, 302, 303].includes(status)
|
|
147
186
|
});
|
|
148
187
|
|
|
149
188
|
const location = res.headers['location'];
|
|
150
189
|
if (location) {
|
|
151
190
|
const match = location.match(/[?&]code=([^&]+)/);
|
|
152
|
-
if (match)
|
|
153
|
-
this.emit('warn', `Form post redirect returned code: ${match[1]}`);
|
|
154
|
-
return match[1];
|
|
155
|
-
}
|
|
191
|
+
if (match) return match[1];
|
|
156
192
|
}
|
|
157
193
|
|
|
158
194
|
this.emit('warn', 'Code not found after form_post submission');
|
|
159
195
|
throw new Error('Code not found after form_post submission');
|
|
160
196
|
}
|
|
161
197
|
|
|
198
|
+
async getTokens(code, codeVerifier) {
|
|
199
|
+
const tokenData = new URLSearchParams({
|
|
200
|
+
grant_type: 'authorization_code',
|
|
201
|
+
code: code,
|
|
202
|
+
redirect_uri: REDIRECT_URI,
|
|
203
|
+
client_id: CLIENT_ID,
|
|
204
|
+
code_verifier: codeVerifier
|
|
205
|
+
});
|
|
162
206
|
|
|
163
|
-
async loginToMelCloudHome(url) {
|
|
164
207
|
try {
|
|
165
|
-
|
|
166
|
-
const getResp = await this.client.get(url, {
|
|
167
|
-
headers: { 'Accept': 'text/html' }
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
const cookies = getResp.headers['set-cookie'] || [];
|
|
171
|
-
const dom = new JSDOM(getResp.data);
|
|
172
|
-
const csrf = dom.window.document.querySelector('input[name="_csrf"]')?.value;
|
|
173
|
-
if (!csrf) {
|
|
174
|
-
this.emit('warn', `CSRF token not found`);
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Step 2: Przygotuj dane logowania
|
|
179
|
-
const formData = new URLSearchParams({
|
|
180
|
-
_csrf: csrf,
|
|
181
|
-
username: this.user,
|
|
182
|
-
password: this.passwd,
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// Step 3: Wyślij POST z danymi logowania
|
|
186
|
-
const response = await this.client.post(url, formData.toString(), {
|
|
208
|
+
const tokenResponse = await this.client.post(TOKEN_ENDPOINT, tokenData.toString(), {
|
|
187
209
|
headers: {
|
|
188
|
-
'User-Agent': MOBILE_USER_AGENT,
|
|
189
|
-
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
190
|
-
'Accept-Language': 'en-US,en;q=0.9',
|
|
191
210
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
192
|
-
'
|
|
193
|
-
'Cookie': cookies.join('; '),
|
|
194
|
-
'Origin': 'https://live-melcloudhome.auth.eu-west-1.amazoncognito.com',
|
|
195
|
-
'Referer': url,
|
|
196
|
-
},
|
|
197
|
-
maxRedirects: 0,
|
|
198
|
-
validateStatus: status => [200, 302, 400].includes(status),
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
if (response.status === 400) {
|
|
202
|
-
this.emit('warn', `Login failed (bad request or invalid credentials): ${response.data}`);
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Step 4: Pobierz Authorization Code
|
|
207
|
-
if (response.status === 200 || response.status === 302) {
|
|
208
|
-
try {
|
|
209
|
-
const code = await this.extractCodeFromResponse(
|
|
210
|
-
response.data, // HTML lub body
|
|
211
|
-
response.headers, // nagłówki
|
|
212
|
-
async (url) => { // followRedirect
|
|
213
|
-
const r = await this.client.get(url, {
|
|
214
|
-
maxRedirects: 0,
|
|
215
|
-
validateStatus: status => [200, 302, 303].includes(status),
|
|
216
|
-
});
|
|
217
|
-
return this.extractCodeFromResponse(r.data, r.headers, followRedirect);
|
|
218
|
-
}
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
if (code) {
|
|
222
|
-
this.emit('warn', `Authorization code obtained: ${code}`);
|
|
223
|
-
return code;
|
|
224
|
-
} else {
|
|
225
|
-
this.emit('warn', 'Authorization code not found in response');
|
|
226
|
-
return null;
|
|
227
|
-
}
|
|
228
|
-
} catch (err) {
|
|
229
|
-
this.emit('warn', `extractCodeFromResponse error: ${err}`);
|
|
230
|
-
return null;
|
|
211
|
+
'Authorization': 'Basic aG9tZW1vYmlsZTo='
|
|
231
212
|
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return null;
|
|
235
|
-
|
|
236
|
-
} catch (error) {
|
|
237
|
-
throw new Error(`Login to MELCloud Home error: ${error}`);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
213
|
+
});
|
|
240
214
|
|
|
215
|
+
const tokens = tokenResponse.data;
|
|
216
|
+
this.emit('warn', `Token obtained: ${JSON.stringify(tokens)}`);
|
|
217
|
+
return tokens;
|
|
241
218
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const pkce = this.generatePKCE();
|
|
245
|
-
const state = this.generateState();
|
|
246
|
-
const authUrl = new URL(AUTHORIZE_ENDPOINT);
|
|
247
|
-
authUrl.searchParams.set('client_id', CLIENT_ID);
|
|
248
|
-
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
|
|
249
|
-
authUrl.searchParams.set('response_type', 'code');
|
|
250
|
-
authUrl.searchParams.set('scope', SCOPE);
|
|
251
|
-
authUrl.searchParams.set('code_challenge', pkce.challenge);
|
|
252
|
-
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
253
|
-
authUrl.searchParams.set('state', state);
|
|
254
|
-
|
|
255
|
-
const loginPage = await this.getLoginPage(authUrl.toString());
|
|
256
|
-
const data = { codeVerifier: pkce.verifier, url: loginPage.url }
|
|
257
|
-
return data;
|
|
258
|
-
} catch (error) {
|
|
259
|
-
throw new Error(`Failed to obtain OAuth link: ${error}`);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
throw new Error(`Failed to obtain OAuth token: ${err}`);
|
|
260
221
|
}
|
|
261
222
|
}
|
|
262
223
|
}
|
|
@@ -267,3 +228,4 @@ export default MelCloudHomeToken;
|
|
|
267
228
|
|
|
268
229
|
|
|
269
230
|
|
|
231
|
+
|