homebridge-melcloud-control 4.0.0-beta.31 → 4.0.0-beta.310

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.
@@ -0,0 +1,179 @@
1
+ import axios from 'axios';
2
+ import crypto from 'crypto';
3
+ import { wrapper } from 'axios-cookiejar-support';
4
+ import { CookieJar } from 'tough-cookie';
5
+ import EventEmitter from 'events';
6
+
7
+ const MOBILE_USER_AGENT = 'MonitorAndControl.App.Mobile/35 CFNetwork/3860.100.1 Darwin/25.0.0';
8
+ const CLIENT_ID = 'homemobile';
9
+ const REDIRECT_URI = 'melcloudhome://';
10
+ const SCOPE = 'openid profile email offline_access IdentityServerApi';
11
+ const TOKEN_ENDPOINT = 'https://auth.melcloudhome.com/connect/token';
12
+ const AUTHORIZE_ENDPOINT = 'https://auth.melcloudhome.com/connect/authorize';
13
+
14
+ class MelCloudHomeToken extends EventEmitter {
15
+ constructor(config) {
16
+ super();
17
+ this.user = config.user;
18
+ this.passwd = config.passwd;
19
+ this.logWarn = config.logWarn;
20
+ this.logError = config.logError;
21
+
22
+ const jar = new CookieJar();
23
+ this.client = wrapper(axios.create({ jar, withCredentials: true }));
24
+ this.client.defaults.headers['User-Agent'] = MOBILE_USER_AGENT;
25
+ }
26
+
27
+ generatePKCE() {
28
+ const verifier = crypto.randomBytes(32).toString('base64url');
29
+ const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
30
+ return { verifier, challenge };
31
+ }
32
+
33
+ generateState() {
34
+ return crypto.randomBytes(32).toString('hex');
35
+ }
36
+
37
+ async getLoginPage(authUrl) {
38
+ const res = await this.client.get(authUrl, { maxRedirects: 10 });
39
+ return { url: res.request.res.responseUrl };
40
+ }
41
+
42
+ async extractCodeFromHtml(html, cookies = '') {
43
+ try {
44
+ // Code w URL
45
+ const urlMatch = html.match(/[?&]code=([^&]+)/);
46
+ if (urlMatch) {
47
+ return urlMatch[1];
48
+ }
49
+
50
+ // Hidden form (form_post)
51
+ const formCodeMatch = html.match(/name="code"\s+value="([^"]+)"/);
52
+ const formStateMatch = html.match(/name="state"\s+value="([^"]+)"/);
53
+ const formActionMatch = html.match(/action="([^"]+)"/);
54
+
55
+ if (formCodeMatch && formStateMatch && formActionMatch) {
56
+ const code = await this.submitFormPost(
57
+ formActionMatch[1],
58
+ formCodeMatch[1],
59
+ formStateMatch[1],
60
+ cookies
61
+ );
62
+ return code;
63
+ }
64
+
65
+ // melcloudhome:// w body
66
+ const bodyCodeMatch = html.match(/melcloudhome:\/\/[^"'\s]*[?&]code=([^&"'\s]+)/);
67
+ if (bodyCodeMatch) {
68
+ return bodyCodeMatch[1];
69
+ }
70
+
71
+ // data-url (np. z błędnej strony 500)
72
+ const dataUrlMatch = html.match(/data-url="[^"]*code=([^&"']+)/);
73
+ if (dataUrlMatch) {
74
+ return dataUrlMatch[1];
75
+ }
76
+
77
+ console.warn('Authorization code not found in HTML response');
78
+ return null;
79
+ } catch (err) {
80
+ console.error('extractCodeFromHtml error:', err);
81
+ return null;
82
+ }
83
+ }
84
+
85
+ async getTokens(code, codeVerifier) {
86
+ try {
87
+ const data = new URLSearchParams({
88
+ grant_type: 'authorization_code',
89
+ code: code,
90
+ redirect_uri: REDIRECT_URI,
91
+ client_id: CLIENT_ID,
92
+ code_verifier: codeVerifier,
93
+ });
94
+
95
+ const res = await axios({
96
+ method: 'POST',
97
+ url: TOKEN_ENDPOINT,
98
+ headers: {
99
+ 'Content-Type': 'application/x-www-form-urlencoded',
100
+ 'Content-Length': data.toString().length
101
+ },
102
+ data: data
103
+ });
104
+ console.log(res);
105
+ if (res.status !== 200) {
106
+ throw new Error(`Token exchange failed: ${res.status} ${res.statusText}`);
107
+ }
108
+
109
+ return res.data;
110
+ } catch (error) {
111
+ console.error('Failed to obtain OAuth tokens:', error);
112
+ throw error;
113
+ }
114
+ }
115
+
116
+ async buildAuthorizeUrl() {
117
+ try {
118
+ const pkce = this.generatePKCE();
119
+ const state = this.generateState();
120
+ const authUrl = new URL(AUTHORIZE_ENDPOINT);
121
+ authUrl.searchParams.set('client_id', CLIENT_ID);
122
+ authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
123
+ authUrl.searchParams.set('response_type', 'code');
124
+ authUrl.searchParams.set('scope', SCOPE);
125
+ authUrl.searchParams.set('code_challenge', pkce.challenge);
126
+ authUrl.searchParams.set('code_challenge_method', 'S256');
127
+ authUrl.searchParams.set('state', state);
128
+
129
+ const loginPage = await this.getLoginPage(authUrl.toString());
130
+ const data = { codeVerifier: pkce.verifier, url: loginPage.url }
131
+ return data;
132
+ } catch (error) {
133
+ console.error('Failed to obtain OAuth link:', error);
134
+ throw error;
135
+ }
136
+ }
137
+
138
+ async loginToMelCloudHome(url) {
139
+ try {
140
+ const form = new URLSearchParams();
141
+ form.append('username[email]', this.user);
142
+ form.append('password[password]', this.passwd);
143
+
144
+ const response = await axios({
145
+ method: 'POST',
146
+ baseURL: url,
147
+ headers: {
148
+ 'Content-Type': 'application/x-www-form-urlencoded'
149
+ },
150
+ data: form,
151
+ timeout: 10000
152
+ });
153
+
154
+ this.emit('warn', `No cookie returned from login. Response headers: ${JSON.stringify(response)}`);
155
+
156
+ if (response.status !== 200) {
157
+ if (this.logError) this.emit('error', `Login failed with status code: ${response.status}`);
158
+ return null;
159
+ }
160
+
161
+ const cookie = response.headers['set-cookie'];
162
+ if (!cookie) {
163
+ if (this.logWarn) this.emit('warn', `No cookie returned from login. Response headers: ${JSON.stringify(response.headers)}`);
164
+ return null;
165
+ }
166
+
167
+ return cookie;
168
+ } catch (error) {
169
+ throw new Error(`Login to MELCloud Home error: ${error}`);
170
+ }
171
+ }
172
+ }
173
+
174
+ export default MelCloudHomeToken;
175
+
176
+
177
+
178
+
179
+