homebridge-melcloud-control 4.0.0-beta.256 → 4.0.0-beta.258

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "displayName": "MELCloud Control",
3
3
  "name": "homebridge-melcloud-control",
4
- "version": "4.0.0-beta.256",
4
+ "version": "4.0.0-beta.258",
5
5
  "description": "Homebridge plugin to control Mitsubishi Air Conditioner, Heat Pump and Energy Recovery Ventilation.",
6
6
  "license": "MIT",
7
7
  "author": "grzegorz914",
@@ -40,7 +40,9 @@
40
40
  "axios": "^1.12.2",
41
41
  "express": "^5.1.0",
42
42
  "puppeteer": "^24.26.1",
43
- "jsdom": "^27.0.1"
43
+ "axios-cookiejar-support": "^6.0.4",
44
+ "tough-cookie": "^6.0.0",
45
+ "cheerio": "^1.1.2"
44
46
  },
45
47
  "keywords": [
46
48
  "homebridge",
@@ -1,9 +1,8 @@
1
- /**
2
- * OAuth Helper - Automatic Token Management (ES Module)
3
- */
4
-
5
- import https from 'https';
1
+ import axios from 'axios';
2
+ import { wrapper } from 'axios-cookiejar-support';
3
+ import { CookieJar } from 'tough-cookie';
6
4
  import crypto from 'crypto';
5
+ import cheerio from 'cheerio';
7
6
 
8
7
  const MOBILE_USER_AGENT = 'MonitorAndControl.App.Mobile/35 CFNetwork/3860.100.1 Darwin/25.0.0';
9
8
  const CLIENT_ID = 'homemobile';
@@ -13,207 +12,158 @@ const TOKEN_ENDPOINT = 'https://auth.melcloudhome.com/connect/token';
13
12
  const AUTHORIZE_ENDPOINT = 'https://auth.melcloudhome.com/connect/authorize';
14
13
 
15
14
  class MelCloudHomeToken {
16
- constructor(log) {
17
- this.log = log;
18
- }
19
-
20
- generatePKCE() {
21
- const verifier = crypto.randomBytes(32).toString('base64url');
22
- const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
23
- return { verifier, challenge };
24
- }
25
-
26
- generateState() {
27
- return crypto.randomBytes(32).toString('hex');
28
- }
29
-
30
- httpsRequest(url, options, body, redirectCount = 0) {
31
- if (redirectCount > 10) return Promise.reject(new Error('Too many redirects'));
32
-
33
- return new Promise((resolve, reject) => {
34
- const urlObj = new URL(url);
35
- const reqOptions = { ...options, hostname: urlObj.hostname, path: urlObj.pathname + urlObj.search };
36
-
37
- const req = https.request(reqOptions, (res) => {
38
- if ([301, 302].includes(res.statusCode) && res.headers.location) {
39
- const redirectUrl = res.headers.location.startsWith('http')
40
- ? res.headers.location
41
- : `https://${urlObj.hostname}${res.headers.location}`;
42
- return this.httpsRequest(redirectUrl, options, undefined, redirectCount + 1)
43
- .then(resolve).catch(reject);
15
+ constructor(log = console) {
16
+ this.log = log;
17
+ const jar = new CookieJar();
18
+ this.client = wrapper(axios.create({ jar, withCredentials: true }));
19
+ this.client.defaults.headers['User-Agent'] = MOBILE_USER_AGENT;
20
+ }
21
+
22
+ generatePKCE() {
23
+ const verifier = crypto.randomBytes(32).toString('base64url');
24
+ const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
25
+ return { verifier, challenge };
26
+ }
27
+
28
+ generateState() {
29
+ return crypto.randomBytes(32).toString('hex');
30
+ }
31
+
32
+ async getLoginPage(authUrl) {
33
+ console.log('Getting login page...');
34
+ const res = await this.client.get(authUrl, { maxRedirects: 10 });
35
+ const $ = cheerio.load(res.data);
36
+ const csrf = $('input[name="_csrf"]').attr('value');
37
+ if (!csrf) throw new Error('Could not extract CSRF token from login page');
38
+ console.log('CSRF token found:', csrf);
39
+ return { csrf, url: res.request.res.responseUrl };
40
+ }
41
+
42
+ async submitFormPost(actionUrl, code, state) {
43
+ console.log('Submitting form_post to callback URL:', actionUrl);
44
+ const form = new URLSearchParams({ code, state });
45
+ const res = await this.client.post(actionUrl, form.toString(), {
46
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
47
+ maxRedirects: 0,
48
+ validateStatus: (status) => status >= 200 && status < 400,
49
+ });
50
+
51
+ const location = res.headers.location;
52
+ if (location?.startsWith('melcloudhome://')) {
53
+ const codeMatch = location.match(/[?&]code=([^&]+)/);
54
+ if (codeMatch) {
55
+ console.log('Authorization code obtained from form_post redirect:', codeMatch[1]);
56
+ return codeMatch[1];
57
+ }
44
58
  }
59
+ throw new Error('No authorization code found in form_post response');
60
+ }
61
+
62
+ async submitLogin(loginUrl, csrf, email, password) {
63
+ console.log('Submitting login credentials to:', loginUrl);
64
+ const form = new URLSearchParams({ _csrf: csrf, username: email, password });
65
+ const res = await this.client.post(loginUrl, form.toString(), {
66
+ headers: {
67
+ 'Content-Type': 'application/x-www-form-urlencoded',
68
+ Referer: loginUrl,
69
+ },
70
+ maxRedirects: 0,
71
+ validateStatus: (status) => status >= 200 && status < 400,
72
+ });
45
73
 
46
- let data = '';
47
- res.on('data', chunk => data += chunk);
48
- res.on('end', () => resolve(data));
49
- });
50
-
51
- req.on('error', reject);
52
- if (body) req.write(body);
53
- req.end();
54
- });
55
- }
56
-
57
- httpsRequestWithCookies(url, options, body, redirectCount = 0, cookies = []) {
58
- if (redirectCount > 10) return Promise.reject(new Error('Too many redirects'));
59
-
60
- return new Promise((resolve, reject) => {
61
- const urlObj = new URL(url);
62
- const reqOptions = { ...options, hostname: urlObj.hostname, path: urlObj.pathname + urlObj.search };
63
-
64
- if (cookies.length) {
65
- reqOptions.headers = { ...(reqOptions.headers || {}), Cookie: cookies.join('; ') };
66
- }
67
-
68
- const req = https.request(reqOptions, (res) => {
69
- const setCookie = res.headers['set-cookie'];
70
- if (setCookie) {
71
- setCookie.forEach(cookie => {
72
- const cookieName = cookie.split('=')[0];
73
- const existingIndex = cookies.findIndex(c => c.startsWith(cookieName + '='));
74
- if (existingIndex >= 0) cookies[existingIndex] = cookie.split(';')[0];
75
- else cookies.push(cookie.split(';')[0]);
76
- });
74
+ console.log('Login POST status:', res.status);
75
+
76
+ // Spróbuj pobrać kod z nagłówka Location
77
+ const location = res.headers.location;
78
+ if (location?.startsWith('melcloudhome://')) {
79
+ const codeMatch = location.match(/[?&]code=([^&]+)/);
80
+ if (codeMatch) {
81
+ console.log('Authorization code obtained from Location header:', codeMatch[1]);
82
+ return codeMatch[1];
83
+ }
84
+ }
85
+
86
+ // Spróbuj parsować HTML w poszukiwaniu form_post
87
+ const $ = cheerio.load(res.data);
88
+ const formElement = $('form');
89
+ if (formElement.length) {
90
+ const action = formElement.attr('action');
91
+ const code = $('input[name="code"]').attr('value');
92
+ const state = $('input[name="state"]').attr('value');
93
+ if (action && code && state) {
94
+ console.log('Found form_post. Action:', action, 'Code:', code, 'State:', state);
95
+ return this.submitFormPost(action, code, state);
96
+ }
77
97
  }
78
98
 
79
- if ([301, 302].includes(res.statusCode) && res.headers.location) {
80
- const redirectUrl = res.headers.location.startsWith('http')
81
- ? res.headers.location
82
- : `https://${urlObj.hostname}${res.headers.location}`;
83
- return this.httpsRequestWithCookies(redirectUrl, options, undefined, redirectCount + 1, cookies)
84
- .then(resolve).catch(reject);
99
+ // Spróbuj wyciągnąć kod z JS w ciele strony
100
+ const bodyCodeMatch = res.data.match(/melcloudhome:\/\/[^"'\s]*[?&]code=([^&"'\s]+)/);
101
+ if (bodyCodeMatch) {
102
+ console.log('Authorization code obtained from HTML/JS body:', bodyCodeMatch[1]);
103
+ return bodyCodeMatch[1];
85
104
  }
86
105
 
87
- let data = '';
88
- res.on('data', chunk => data += chunk);
89
- res.on('end', () => resolve({ html: data, cookies, finalUrl: url }));
90
- });
91
-
92
- req.on('error', reject);
93
- if (body) req.write(body);
94
- req.end();
95
- });
96
- }
97
-
98
- async getLoginPage(authUrl) {
99
- console.log('Getting login page...');
100
- const cookies = [];
101
- const response = await this.httpsRequestWithCookies(authUrl, {
102
- method: 'GET',
103
- headers: {
104
- 'User-Agent': MOBILE_USER_AGENT,
105
- Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
106
- 'Accept-Language': 'en-US,en;q=0.9',
107
- },
108
- }, undefined, 0, cookies);
109
-
110
- const csrfMatch = response.html.match(/name="_csrf"\s+value="([^"]+)"/);
111
- if (!csrfMatch) throw new Error('Could not extract CSRF token from login page');
112
-
113
- return {
114
- html: response.html,
115
- loginUrl: response.finalUrl,
116
- csrf: csrfMatch[1],
117
- cookies: response.cookies.join('; '),
118
- };
119
- }
120
-
121
- submitLogin(loginUrl, csrf, email, password, cookies) {
122
- const formData = new URLSearchParams({ _csrf: csrf, username: email, password });
123
- return new Promise((resolve, reject) => {
124
- const urlObj = new URL(loginUrl);
125
- const req = https.request({
126
- hostname: urlObj.hostname,
127
- path: urlObj.pathname + urlObj.search,
128
- method: 'POST',
129
- headers: {
130
- 'User-Agent': MOBILE_USER_AGENT,
131
- Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
132
- 'Accept-Language': 'en-US,en;q=0.9',
133
- 'Content-Type': 'application/x-www-form-urlencoded',
134
- 'Content-Length': formData.toString().length,
135
- Cookie: cookies,
136
- Origin: 'https://live-melcloudhome.auth.eu-west-1.amazoncognito.com',
137
- Referer: loginUrl,
138
- },
139
- }, (res) => {
140
- let data = '';
141
- res.on('data', chunk => data += chunk);
142
- res.on('end', () => {
143
- const locationHeader = res.headers.location;
144
- const bodyCodeMatch = data.match(/melcloudhome:\/\/[^"'\s]*[?&]code=([^&"'\s]+)/);
145
- if (locationHeader && locationHeader.startsWith('melcloudhome://')) {
146
- const codeMatch = locationHeader.match(/[?&]code=([^&]+)/);
147
- if (codeMatch) return resolve(codeMatch[1]);
148
- } else if (bodyCodeMatch) return resolve(bodyCodeMatch[1]);
149
- reject(new Error('No authorization code found in login response'));
106
+ throw new Error('Authorization code not found in login response');
107
+ }
108
+
109
+ async exchangeCodeForTokens(code, codeVerifier) {
110
+ console.log('Exchanging authorization code for tokens...');
111
+ const form = new URLSearchParams({
112
+ grant_type: 'authorization_code',
113
+ code,
114
+ redirect_uri: REDIRECT_URI,
115
+ client_id: CLIENT_ID,
116
+ code_verifier: codeVerifier,
117
+ });
118
+
119
+ const res = await this.client.post(TOKEN_ENDPOINT, form.toString(), {
120
+ headers: {
121
+ 'Content-Type': 'application/x-www-form-urlencoded',
122
+ Authorization: 'Basic aG9tZW1vYmlsZTo=',
123
+ },
150
124
  });
151
- });
152
-
153
- req.on('error', reject);
154
- req.write(formData.toString());
155
- req.end();
156
- });
157
- }
158
-
159
- async exchangeCodeForTokens(code, codeVerifier) {
160
- const tokenData = new URLSearchParams({
161
- grant_type: 'authorization_code',
162
- code,
163
- redirect_uri: REDIRECT_URI,
164
- client_id: CLIENT_ID,
165
- code_verifier: codeVerifier,
166
- });
167
-
168
- const response = await this.httpsRequest(TOKEN_ENDPOINT, {
169
- method: 'POST',
170
- headers: {
171
- 'Content-Type': 'application/x-www-form-urlencoded',
172
- 'Content-Length': tokenData.toString().length,
173
- Authorization: 'Basic aG9tZW1vYmlsZTo=',
174
- },
175
- }, tokenData.toString());
176
-
177
- const tokens = JSON.parse(response);
178
- if (tokens.error) throw new Error(`Token exchange failed: ${tokens.error_description || tokens.error}`);
179
-
180
- return {
181
- accessToken: tokens.access_token,
182
- refreshToken: tokens.refresh_token,
183
- expiresIn: tokens.expires_in,
184
- idToken: tokens.id_token,
185
- };
186
- }
187
-
188
- async getTokensWithCredentials(email, password) {
189
- try {
190
- console.log('Obtaining OAuth tokens automatically...');
191
- const pkce = this.generatePKCE();
192
- const state = this.generateState();
193
-
194
- const authUrl = new URL(AUTHORIZE_ENDPOINT);
195
- authUrl.searchParams.set('client_id', CLIENT_ID);
196
- authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
197
- authUrl.searchParams.set('response_type', 'code');
198
- authUrl.searchParams.set('scope', SCOPE);
199
- authUrl.searchParams.set('code_challenge', pkce.challenge);
200
- authUrl.searchParams.set('code_challenge_method', 'S256');
201
- authUrl.searchParams.set('state', state);
202
-
203
- const loginPage = await this.getLoginPage(authUrl.toString());
204
- const authCode = await this.submitLogin(loginPage.loginUrl, loginPage.csrf, email, password, loginPage.cookies);
205
- const tokens = await this.exchangeCodeForTokens(authCode, pkce.verifier);
206
-
207
- console.log('✓ Successfully obtained OAuth tokens');
208
- return tokens;
209
- } catch (error) {
210
- console.log('Failed to obtain OAuth tokens:', error);
211
- throw error;
125
+
126
+ if (res.data.error) throw new Error(`Token exchange failed: ${res.data.error_description || res.data.error}`);
127
+
128
+ console.log('Tokens obtained:', res.data);
129
+ return {
130
+ accessToken: res.data.access_token,
131
+ refreshToken: res.data.refresh_token,
132
+ expiresIn: res.data.expires_in,
133
+ idToken: res.data.id_token,
134
+ };
135
+ }
136
+
137
+ async getTokensWithCredentials(email, password) {
138
+ try {
139
+ console.log('Starting OAuth flow...');
140
+ const pkce = this.generatePKCE();
141
+ const state = this.generateState();
142
+
143
+ const authUrl = new URL(AUTHORIZE_ENDPOINT);
144
+ authUrl.searchParams.set('client_id', CLIENT_ID);
145
+ authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
146
+ authUrl.searchParams.set('response_type', 'code');
147
+ authUrl.searchParams.set('scope', SCOPE);
148
+ authUrl.searchParams.set('code_challenge', pkce.challenge);
149
+ authUrl.searchParams.set('code_challenge_method', 'S256');
150
+ authUrl.searchParams.set('state', state);
151
+
152
+ const loginPage = await this.getLoginPage(authUrl.toString());
153
+ const authCode = await this.submitLogin(loginPage.url, loginPage.csrf, email, password);
154
+ const tokens = await this.exchangeCodeForTokens(authCode, pkce.verifier);
155
+
156
+ console.log('✓ Successfully obtained OAuth tokens');
157
+ return tokens;
158
+ } catch (error) {
159
+ console.error('Failed to obtain OAuth tokens:', error);
160
+ throw error;
161
+ }
212
162
  }
213
- }
214
163
  }
215
164
 
216
165
 
166
+
217
167
  export default MelCloudHomeToken;
218
168
 
219
169