homebridge-melcloud-control 4.0.0-beta.374 → 4.0.0-beta.376
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 +58 -119
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.376",
|
|
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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
2
|
import crypto from 'crypto';
|
|
3
|
+
import { wrapper } from 'axios-cookiejar-support';
|
|
4
|
+
import { CookieJar } from 'tough-cookie';
|
|
3
5
|
import { JSDOM } from 'jsdom';
|
|
4
6
|
import EventEmitter from 'events';
|
|
5
7
|
|
|
@@ -17,7 +19,10 @@ class MelCloudHomeToken extends EventEmitter {
|
|
|
17
19
|
this.passwd = config.passwd;
|
|
18
20
|
this.logWarn = config.logWarn;
|
|
19
21
|
this.logError = config.logError;
|
|
20
|
-
|
|
22
|
+
|
|
23
|
+
const jar = new CookieJar();
|
|
24
|
+
this.axiosInstance = wrapper(axios.create({ jar, withCredentials: true }));
|
|
25
|
+
this.client.defaults.headers['User-Agent'] = MOBILE_USER_AGENT;
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
generatePKCE() {
|
|
@@ -31,97 +36,50 @@ class MelCloudHomeToken extends EventEmitter {
|
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
async getLoginPage(authUrl) {
|
|
34
|
-
const res = await this.
|
|
39
|
+
const res = await this.client.get(authUrl, { maxRedirects: 10 });
|
|
35
40
|
return { url: res.request.res.responseUrl };
|
|
36
41
|
}
|
|
37
42
|
|
|
38
|
-
async extractCodeFromHtml(html, cookies = '') {
|
|
39
|
-
try {
|
|
40
|
-
// Code w URL
|
|
41
|
-
const urlMatch = html.match(/[?&]code=([^&]+)/);
|
|
42
|
-
if (urlMatch) {
|
|
43
|
-
return urlMatch[1];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Hidden form (form_post)
|
|
47
|
-
const formCodeMatch = html.match(/name="code"\s+value="([^"]+)"/);
|
|
48
|
-
const formStateMatch = html.match(/name="state"\s+value="([^"]+)"/);
|
|
49
|
-
const formActionMatch = html.match(/action="([^"]+)"/);
|
|
50
|
-
|
|
51
|
-
if (formCodeMatch && formStateMatch && formActionMatch) {
|
|
52
|
-
const code = await this.submitFormPost(
|
|
53
|
-
formActionMatch[1],
|
|
54
|
-
formCodeMatch[1],
|
|
55
|
-
formStateMatch[1],
|
|
56
|
-
cookies
|
|
57
|
-
);
|
|
58
|
-
return code;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// melcloudhome:// w body
|
|
62
|
-
const bodyCodeMatch = html.match(/melcloudhome:\/\/[^"'\s]*[?&]code=([^&"'\s]+)/);
|
|
63
|
-
if (bodyCodeMatch) {
|
|
64
|
-
return bodyCodeMatch[1];
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// data-url (np. z błędnej strony 500)
|
|
68
|
-
const dataUrlMatch = html.match(/data-url="[^"]*code=([^&"']+)/);
|
|
69
|
-
if (dataUrlMatch) {
|
|
70
|
-
return dataUrlMatch[1];
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
console.warn('Authorization code not found in HTML response');
|
|
74
|
-
return null;
|
|
75
|
-
} catch (err) {
|
|
76
|
-
console.error('extractCodeFromHtml error:', err);
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
43
|
async getTokens(code, codeVerifier) {
|
|
82
44
|
try {
|
|
83
45
|
const tokenData = new URLSearchParams({
|
|
84
46
|
grant_type: 'authorization_code',
|
|
85
|
-
code,
|
|
47
|
+
code: code,
|
|
86
48
|
redirect_uri: REDIRECT_URI,
|
|
87
49
|
client_id: CLIENT_ID,
|
|
88
50
|
code_verifier: codeVerifier,
|
|
89
51
|
});
|
|
90
52
|
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
'Authorization': 'Basic aG9tZW1vYmlsZTo=', // optional, Cognito may ignore
|
|
98
|
-
},
|
|
99
|
-
}
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
this.emit('warn', `Token response: ${JSON.stringify(response.data)}`);
|
|
103
|
-
return response.data;
|
|
53
|
+
const tokenResponse = await this.client.post(TOKEN_ENDPOINT, tokenData.toString(), {
|
|
54
|
+
headers: {
|
|
55
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
56
|
+
'Authorization': 'Basic aG9tZW1vYmlsZTo=',
|
|
57
|
+
},
|
|
58
|
+
});
|
|
104
59
|
|
|
60
|
+
const tokens = tokenResponse.data;
|
|
61
|
+
this.emit('warn', `Token ${JSON.stringify(tokens)}`);
|
|
62
|
+
return tokens;
|
|
105
63
|
} catch (error) {
|
|
106
|
-
throw new Error(`Failed to obtain OAuth token: ${error
|
|
64
|
+
throw new Error(`Failed to obtain OAuth token: ${error}`);
|
|
107
65
|
}
|
|
108
66
|
}
|
|
109
67
|
|
|
110
|
-
|
|
111
68
|
async loginToMelCloudHome(url) {
|
|
112
69
|
try {
|
|
113
|
-
// Step 1: Pobierz stronę logowania
|
|
114
|
-
const getResp = await this.
|
|
115
|
-
headers: {
|
|
116
|
-
|
|
70
|
+
// Step 1: Pobierz stronę logowania, żeby uzyskać sesję + token _csrf
|
|
71
|
+
const getResp = await this.client.get(url, {
|
|
72
|
+
headers: {
|
|
73
|
+
'User-Agent': MOBILE_USER_AGENT,
|
|
74
|
+
'Accept': 'text/html',
|
|
75
|
+
}
|
|
117
76
|
});
|
|
118
77
|
|
|
119
|
-
const cookies =
|
|
78
|
+
const cookies = getResp.headers['set-cookie'] || [];
|
|
120
79
|
const dom = new JSDOM(getResp.data);
|
|
121
80
|
const csrf = dom.window.document.querySelector('input[name="_csrf"]')?.value;
|
|
122
|
-
|
|
123
81
|
if (!csrf) {
|
|
124
|
-
this.emit('warn', `CSRF token not found`);
|
|
82
|
+
if (this.logWarn) this.emit('warn', `CSRF token not found`);
|
|
125
83
|
return null;
|
|
126
84
|
}
|
|
127
85
|
|
|
@@ -132,66 +90,47 @@ class MelCloudHomeToken extends EventEmitter {
|
|
|
132
90
|
password: this.passwd,
|
|
133
91
|
});
|
|
134
92
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
{
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
if (postResp.status === 400) {
|
|
153
|
-
this.emit('warn', `Login failed: ${postResp.data}`);
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
let redirectUrl = postResp.headers.location;
|
|
158
|
-
let cookieChain = cookies;
|
|
159
|
-
|
|
160
|
-
// Step 4: Podążaj za przekierowaniami
|
|
161
|
-
for (let i = 0; i < 5 && redirectUrl; i++) {
|
|
162
|
-
const redirectResp = await this.axiosInstance.get(redirectUrl, {
|
|
163
|
-
headers: { 'Cookie': cookieChain },
|
|
164
|
-
maxRedirects: 0,
|
|
165
|
-
validateStatus: s => [200, 302].includes(s),
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
// aktualizuj cookies
|
|
169
|
-
const newCookies = redirectResp.headers['set-cookie'] || [];
|
|
170
|
-
if (newCookies.length) {
|
|
171
|
-
cookieChain += '; ' + newCookies.map(c => c.split(';')[0]).join('; ');
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
redirectUrl = redirectResp.headers.location;
|
|
93
|
+
if (this.logWarn) this.emit('warn', `Redirect URL: ${url}`);
|
|
94
|
+
|
|
95
|
+
// Step 3: Wyślij POST z danymi logowania
|
|
96
|
+
const response = await this.client.post(url, formData.toString(), {
|
|
97
|
+
headers: {
|
|
98
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
99
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
100
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
101
|
+
'Content-Length': formData.toString().length,
|
|
102
|
+
'Cookie': cookies,
|
|
103
|
+
'Origin': 'https://live-melcloudhome.auth.eu-west-1.amazoncognito.com',
|
|
104
|
+
'Referer': url,
|
|
105
|
+
},
|
|
106
|
+
maxRedirects: 0,
|
|
107
|
+
validateStatus: status => [200, 302, 400].includes(status),
|
|
108
|
+
});
|
|
175
109
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
110
|
+
if (response.status === 400) if (this.logWarn) this.emit('warn', `Login failed (bad request or invalid credentials: ${response.data}`);
|
|
111
|
+
if (response.status === 302 && response.headers.location) {
|
|
112
|
+
// Login success — mamy redirect z "code="
|
|
113
|
+
const redirectUrl = response.headers.location;
|
|
114
|
+
if (this.logWarn) this.emit('warn', `Redirect URL: ${redirectUrl}`);
|
|
115
|
+
if (this.logWarn) this.emit('warn', `Response headers: ${response.headers}`);
|
|
116
|
+
|
|
117
|
+
const match = redirectUrl.match(/[?&]code=([^&]+)/);
|
|
118
|
+
if (match) {
|
|
119
|
+
const code = match[1];
|
|
120
|
+
if (this.logWarn) this.emit('warn', `Redirect URL found code: ${code}`);
|
|
121
|
+
return code;
|
|
122
|
+
} else {
|
|
123
|
+
if (this.logWarn) this.emit('warn', `Redirect URL found, but no "code=" param: ${redirectUrl}`);
|
|
124
|
+
return null;
|
|
183
125
|
}
|
|
184
126
|
}
|
|
185
127
|
|
|
186
|
-
this.emit('warn', 'Login flow finished, but no code found');
|
|
187
128
|
return null;
|
|
188
|
-
|
|
189
129
|
} catch (error) {
|
|
190
|
-
throw new Error(`Login to MELCloud Home error: ${error
|
|
130
|
+
throw new Error(`Login to MELCloud Home error: ${error}`);
|
|
191
131
|
}
|
|
192
132
|
}
|
|
193
133
|
|
|
194
|
-
|
|
195
134
|
async buildAuthorizeUrl() {
|
|
196
135
|
try {
|
|
197
136
|
const pkce = this.generatePKCE();
|