homey-api 3.17.6 → 3.17.7
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.
|
@@ -5748,6 +5748,8 @@ export class Util {
|
|
|
5748
5748
|
timeoutDuration?: number,
|
|
5749
5749
|
|
|
5750
5750
|
timeoutMessage?: string,
|
|
5751
|
+
|
|
5752
|
+
patchOptions?: Function,
|
|
5751
5753
|
): Promise<any>;
|
|
5752
5754
|
|
|
5753
5755
|
static wait(ms: number): Promise<void>;
|
|
@@ -5790,6 +5792,8 @@ export class Util {
|
|
|
5790
5792
|
timeoutDuration?: number,
|
|
5791
5793
|
|
|
5792
5794
|
timeoutMessage?: string,
|
|
5795
|
+
|
|
5796
|
+
patchOptions?: Function,
|
|
5793
5797
|
): Promise<any>;
|
|
5794
5798
|
|
|
5795
5799
|
static wait(ms: number): Promise<void>;
|
|
@@ -9166,6 +9170,8 @@ export class Util {
|
|
|
9166
9170
|
timeoutDuration?: number,
|
|
9167
9171
|
|
|
9168
9172
|
timeoutMessage?: string,
|
|
9173
|
+
|
|
9174
|
+
patchOptions?: Function,
|
|
9169
9175
|
): Promise<any>;
|
|
9170
9176
|
|
|
9171
9177
|
static wait(ms: number): Promise<void>;
|
|
@@ -9208,6 +9214,8 @@ export class Util {
|
|
|
9208
9214
|
timeoutDuration?: number,
|
|
9209
9215
|
|
|
9210
9216
|
timeoutMessage?: string,
|
|
9217
|
+
|
|
9218
|
+
patchOptions?: Function,
|
|
9211
9219
|
): Promise<any>;
|
|
9212
9220
|
|
|
9213
9221
|
static wait(ms: number): Promise<void>;
|
|
@@ -5,9 +5,16 @@ const APIErrorHomeySubscriptionInactive = require('../../APIErrorHomeySubscripti
|
|
|
5
5
|
const APIErrorHomeyInvalidSerialNumber = require('../../APIErrorHomeyInvalidSerialNumber');
|
|
6
6
|
const Util = require('../../Util');
|
|
7
7
|
|
|
8
|
+
function getRuntimeRequire() {
|
|
9
|
+
// Avoid webpack statically bundling Node-only discovery transport into the browser build.
|
|
10
|
+
// eslint-disable-next-line no-eval
|
|
11
|
+
return eval('require');
|
|
12
|
+
}
|
|
13
|
+
|
|
8
14
|
class DiscoveryManager {
|
|
9
15
|
constructor(homey) {
|
|
10
16
|
this.homey = homey;
|
|
17
|
+
this.discoveryNodeTransport = null;
|
|
11
18
|
}
|
|
12
19
|
|
|
13
20
|
async discoverBaseUrl() {
|
|
@@ -17,7 +24,10 @@ class DiscoveryManager {
|
|
|
17
24
|
if (this.homey.__strategies.includes(DISCOVERY_STRATEGIES.MDNS)) {
|
|
18
25
|
if (Util.isHTTPUnsecureSupported()) {
|
|
19
26
|
if (this.homey.__properties.mdnsFqdn) {
|
|
20
|
-
const port =
|
|
27
|
+
const port =
|
|
28
|
+
this.homey.__properties.portHttp && this.homey.__properties.portHttp !== 80
|
|
29
|
+
? `:${this.homey.__properties.portHttp}`
|
|
30
|
+
: '';
|
|
21
31
|
urls[DISCOVERY_STRATEGIES.MDNS] = `http://${this.homey.__properties.mdnsFqdn}${port}`;
|
|
22
32
|
} else {
|
|
23
33
|
urls[DISCOVERY_STRATEGIES.MDNS] = `http://homey-${this.homey.id}.local`;
|
|
@@ -112,18 +122,24 @@ class DiscoveryManager {
|
|
|
112
122
|
|
|
113
123
|
const ping = async (strategyId, timeout) => {
|
|
114
124
|
const baseUrl = urls[strategyId];
|
|
125
|
+
const pingUrl = `${baseUrl}/api/manager/system/ping?id=${this.homey.id}`;
|
|
115
126
|
const abortController = new AbortController();
|
|
116
127
|
pingAbortControllers[strategyId] = abortController;
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
128
|
+
const discoveryTransport = this.getDiscoveryNodeTransport();
|
|
129
|
+
const fetch =
|
|
130
|
+
discoveryTransport && discoveryTransport.shouldUseForUrl(pingUrl)
|
|
131
|
+
? discoveryTransport.fetch.bind(discoveryTransport)
|
|
132
|
+
: Util.fetch.bind(Util);
|
|
133
|
+
|
|
134
|
+
const response = await fetch(
|
|
135
|
+
pingUrl,
|
|
120
136
|
{
|
|
121
137
|
headers: {
|
|
122
138
|
'X-Homey-ID': this.homey.id,
|
|
123
139
|
},
|
|
124
140
|
signal: abortController.signal,
|
|
125
141
|
},
|
|
126
|
-
timeout
|
|
142
|
+
timeout,
|
|
127
143
|
);
|
|
128
144
|
|
|
129
145
|
const text = await response.text();
|
|
@@ -180,15 +196,9 @@ class DiscoveryManager {
|
|
|
180
196
|
const pings = {};
|
|
181
197
|
|
|
182
198
|
if (urls[DISCOVERY_STRATEGIES.LOCAL_SECURE]) {
|
|
183
|
-
pings[DISCOVERY_STRATEGIES.LOCAL_SECURE] = ping(
|
|
184
|
-
DISCOVERY_STRATEGIES.LOCAL_SECURE,
|
|
185
|
-
1200
|
|
186
|
-
);
|
|
199
|
+
pings[DISCOVERY_STRATEGIES.LOCAL_SECURE] = ping(DISCOVERY_STRATEGIES.LOCAL_SECURE, 1200);
|
|
187
200
|
pings[DISCOVERY_STRATEGIES.LOCAL_SECURE].catch((err) => {
|
|
188
|
-
this.homey.__debug(
|
|
189
|
-
`Ping ${DISCOVERY_STRATEGIES.LOCAL_SECURE} Error:`,
|
|
190
|
-
err && err.message
|
|
191
|
-
);
|
|
201
|
+
this.homey.__debug(`Ping ${DISCOVERY_STRATEGIES.LOCAL_SECURE} Error:`, err && err.message);
|
|
192
202
|
this.homey.__debug(urls[DISCOVERY_STRATEGIES.LOCAL_SECURE]);
|
|
193
203
|
});
|
|
194
204
|
}
|
|
@@ -196,34 +206,34 @@ class DiscoveryManager {
|
|
|
196
206
|
if (urls[DISCOVERY_STRATEGIES.LOCAL]) {
|
|
197
207
|
pings[DISCOVERY_STRATEGIES.LOCAL] = ping(DISCOVERY_STRATEGIES.LOCAL, 1000);
|
|
198
208
|
pings[DISCOVERY_STRATEGIES.LOCAL].catch((err) =>
|
|
199
|
-
this.homey.__debug(`Ping ${DISCOVERY_STRATEGIES.LOCAL} Error:`, err && err.message)
|
|
209
|
+
this.homey.__debug(`Ping ${DISCOVERY_STRATEGIES.LOCAL} Error:`, err && err.message),
|
|
200
210
|
);
|
|
201
211
|
}
|
|
202
212
|
|
|
203
213
|
if (urls[DISCOVERY_STRATEGIES.MDNS]) {
|
|
204
214
|
pings[DISCOVERY_STRATEGIES.MDNS] = ping(DISCOVERY_STRATEGIES.MDNS, 3000);
|
|
205
215
|
pings[DISCOVERY_STRATEGIES.MDNS].catch((err) =>
|
|
206
|
-
this.homey.__debug(`Ping ${DISCOVERY_STRATEGIES.MDNS} Error:`, err && err.message)
|
|
216
|
+
this.homey.__debug(`Ping ${DISCOVERY_STRATEGIES.MDNS} Error:`, err && err.message),
|
|
207
217
|
);
|
|
208
218
|
}
|
|
209
219
|
|
|
210
220
|
if (urls[DISCOVERY_STRATEGIES.CLOUD]) {
|
|
211
221
|
pings[DISCOVERY_STRATEGIES.CLOUD] = ping(DISCOVERY_STRATEGIES.CLOUD, 5000);
|
|
212
222
|
pings[DISCOVERY_STRATEGIES.CLOUD].catch((err) =>
|
|
213
|
-
this.homey.__debug(`Ping ${DISCOVERY_STRATEGIES.CLOUD} Error:`, err && err.message)
|
|
223
|
+
this.homey.__debug(`Ping ${DISCOVERY_STRATEGIES.CLOUD} Error:`, err && err.message),
|
|
214
224
|
);
|
|
215
225
|
}
|
|
216
226
|
|
|
217
227
|
if (urls[DISCOVERY_STRATEGIES.REMOTE_FORWARDED]) {
|
|
218
228
|
pings[DISCOVERY_STRATEGIES.REMOTE_FORWARDED] = ping(
|
|
219
229
|
DISCOVERY_STRATEGIES.REMOTE_FORWARDED,
|
|
220
|
-
2000
|
|
230
|
+
2000,
|
|
221
231
|
);
|
|
222
232
|
pings[DISCOVERY_STRATEGIES.REMOTE_FORWARDED].catch((err) =>
|
|
223
233
|
this.homey.__debug(
|
|
224
234
|
`Ping ${DISCOVERY_STRATEGIES.REMOTE_FORWARDED} Error:`,
|
|
225
|
-
err && err.message
|
|
226
|
-
)
|
|
235
|
+
err && err.message,
|
|
236
|
+
),
|
|
227
237
|
);
|
|
228
238
|
}
|
|
229
239
|
|
|
@@ -240,36 +250,35 @@ class DiscoveryManager {
|
|
|
240
250
|
let selectedRoutePromise;
|
|
241
251
|
|
|
242
252
|
if (pings[DISCOVERY_STRATEGIES.LOCAL_SECURE]) {
|
|
243
|
-
selectedRoutePromise = pings[DISCOVERY_STRATEGIES.LOCAL_SECURE]
|
|
244
|
-
|
|
245
|
-
const fallbackPromises = [];
|
|
253
|
+
selectedRoutePromise = pings[DISCOVERY_STRATEGIES.LOCAL_SECURE].catch((error) => {
|
|
254
|
+
const fallbackPromises = [];
|
|
246
255
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
256
|
+
if (pings[DISCOVERY_STRATEGIES.LOCAL]) {
|
|
257
|
+
fallbackPromises.push(pings[DISCOVERY_STRATEGIES.LOCAL]);
|
|
258
|
+
}
|
|
250
259
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
260
|
+
if (pings[DISCOVERY_STRATEGIES.REMOTE_FORWARDED]) {
|
|
261
|
+
fallbackPromises.push(pings[DISCOVERY_STRATEGIES.REMOTE_FORWARDED]);
|
|
262
|
+
}
|
|
254
263
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
264
|
+
if (pings[DISCOVERY_STRATEGIES.MDNS]) {
|
|
265
|
+
fallbackPromises.push(pings[DISCOVERY_STRATEGIES.MDNS]);
|
|
266
|
+
}
|
|
258
267
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
268
|
+
if (pings[DISCOVERY_STRATEGIES.CLOUD]) {
|
|
269
|
+
fallbackPromises.push(pings[DISCOVERY_STRATEGIES.CLOUD]);
|
|
270
|
+
}
|
|
262
271
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
272
|
+
if (isSubscriptionError(error)) {
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
266
275
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
276
|
+
if (!fallbackPromises.length) {
|
|
277
|
+
throw new APIErrorHomeyOffline();
|
|
278
|
+
}
|
|
270
279
|
|
|
271
|
-
|
|
272
|
-
|
|
280
|
+
return Promise.any(fallbackPromises);
|
|
281
|
+
});
|
|
273
282
|
} else if (pings[DISCOVERY_STRATEGIES.LOCAL]) {
|
|
274
283
|
selectedRoutePromise = withCloudFallback(pings[DISCOVERY_STRATEGIES.LOCAL]);
|
|
275
284
|
} else if (pings[DISCOVERY_STRATEGIES.MDNS]) {
|
|
@@ -290,6 +299,17 @@ class DiscoveryManager {
|
|
|
290
299
|
|
|
291
300
|
return promise;
|
|
292
301
|
}
|
|
302
|
+
|
|
303
|
+
getDiscoveryNodeTransport() {
|
|
304
|
+
if (!Util.isNodeJS()) {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
this.discoveryNodeTransport =
|
|
309
|
+
this.discoveryNodeTransport || getRuntimeRequire()('./DiscoveryNodeTransport');
|
|
310
|
+
|
|
311
|
+
return this.discoveryNodeTransport;
|
|
312
|
+
}
|
|
293
313
|
}
|
|
294
314
|
|
|
295
|
-
module.exports = DiscoveryManager;
|
|
315
|
+
module.exports = DiscoveryManager;
|
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Util = require('../../Util');
|
|
4
|
+
|
|
5
|
+
const DNS_RECORD_TYPES = {
|
|
6
|
+
A: 0x0001,
|
|
7
|
+
AAAA: 0x001c,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const DNS_CLASS_IN = 0x0001;
|
|
11
|
+
const DNS_CLASS_QU = 0x8000;
|
|
12
|
+
|
|
13
|
+
function getNodeHttpModules() {
|
|
14
|
+
return {
|
|
15
|
+
http: require('node:http'),
|
|
16
|
+
https: require('node:https'),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getDnsModule() {
|
|
21
|
+
return require('node:dns');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getDgramModule() {
|
|
25
|
+
return require('node:dgram');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getNetModule() {
|
|
29
|
+
return require('node:net');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createAbortError(reason) {
|
|
33
|
+
const error = new Error(
|
|
34
|
+
typeof reason === 'string' && reason.length > 0 ? reason : 'The operation was aborted',
|
|
35
|
+
);
|
|
36
|
+
error.name = 'AbortError';
|
|
37
|
+
error.code = 'ABORT_ERR';
|
|
38
|
+
error.type = 'aborted';
|
|
39
|
+
return error;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getHostname(url) {
|
|
43
|
+
try {
|
|
44
|
+
return new URL(url).hostname;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeHostname(hostname) {
|
|
51
|
+
return String(hostname || '')
|
|
52
|
+
.replace(/\.$/, '')
|
|
53
|
+
.toLowerCase();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function shouldUseResolverLookupForHostname(hostname) {
|
|
57
|
+
return normalizeHostname(hostname).endsWith('.homey.homeylocal.com');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function shouldUseMdnsLookupForHostname(hostname) {
|
|
61
|
+
return normalizeHostname(hostname).endsWith('.local');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getMdnsQueryTypes(options) {
|
|
65
|
+
const family = Number(options?.family) || 0;
|
|
66
|
+
|
|
67
|
+
if (family === 6) {
|
|
68
|
+
return [DNS_RECORD_TYPES.AAAA];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return [DNS_RECORD_TYPES.A];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getMdnsConfig() {
|
|
75
|
+
const host = process.env.HOMEY_DISCOVERY_MDNS_HOST || '224.0.0.251';
|
|
76
|
+
const port = Number(process.env.HOMEY_DISCOVERY_MDNS_PORT) || 5353;
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
host,
|
|
80
|
+
port,
|
|
81
|
+
joinMulticast: host === '224.0.0.251',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function encodeDnsName(hostname) {
|
|
86
|
+
const labels = normalizeHostname(hostname).split('.');
|
|
87
|
+
const parts = [];
|
|
88
|
+
|
|
89
|
+
for (const label of labels) {
|
|
90
|
+
const encodedLabel = Buffer.from(label);
|
|
91
|
+
parts.push(Buffer.from([encodedLabel.length]));
|
|
92
|
+
parts.push(encodedLabel);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
parts.push(Buffer.from([0]));
|
|
96
|
+
|
|
97
|
+
return Buffer.concat(parts);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function buildMdnsQuestion(hostname, type) {
|
|
101
|
+
const question = Buffer.alloc(4);
|
|
102
|
+
question.writeUInt16BE(type, 0);
|
|
103
|
+
question.writeUInt16BE(DNS_CLASS_IN | DNS_CLASS_QU, 2);
|
|
104
|
+
|
|
105
|
+
return Buffer.concat([encodeDnsName(hostname), question]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function buildMdnsQuery(hostname, options) {
|
|
109
|
+
const types = getMdnsQueryTypes(options);
|
|
110
|
+
const header = Buffer.alloc(12);
|
|
111
|
+
|
|
112
|
+
header.writeUInt16BE(0, 0);
|
|
113
|
+
header.writeUInt16BE(0, 2);
|
|
114
|
+
header.writeUInt16BE(types.length, 4);
|
|
115
|
+
|
|
116
|
+
return Buffer.concat([
|
|
117
|
+
header,
|
|
118
|
+
...types.map((type) => buildMdnsQuestion(hostname, type)),
|
|
119
|
+
]);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function readDnsName(buffer, offset, visited = new Set()) {
|
|
123
|
+
const labels = [];
|
|
124
|
+
let cursor = offset;
|
|
125
|
+
|
|
126
|
+
while (cursor < buffer.length) {
|
|
127
|
+
const length = buffer[cursor];
|
|
128
|
+
|
|
129
|
+
if (length === 0) {
|
|
130
|
+
return {
|
|
131
|
+
name: labels.join('.'),
|
|
132
|
+
offset: cursor + 1,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if ((length & 0xc0) === 0xc0) {
|
|
137
|
+
if (cursor + 1 >= buffer.length) {
|
|
138
|
+
throw new Error('Invalid DNS name pointer');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const pointer = ((length & 0x3f) << 8) | buffer[cursor + 1];
|
|
142
|
+
|
|
143
|
+
if (visited.has(pointer)) {
|
|
144
|
+
throw new Error('Invalid DNS name compression loop');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
visited.add(pointer);
|
|
148
|
+
|
|
149
|
+
const result = readDnsName(buffer, pointer, visited);
|
|
150
|
+
|
|
151
|
+
if (result.name) {
|
|
152
|
+
labels.push(result.name);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
name: labels.join('.'),
|
|
157
|
+
offset: cursor + 2,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const labelStart = cursor + 1;
|
|
162
|
+
const labelEnd = labelStart + length;
|
|
163
|
+
|
|
164
|
+
if (labelEnd > buffer.length) {
|
|
165
|
+
throw new Error('Invalid DNS label length');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
labels.push(buffer.toString('utf8', labelStart, labelEnd));
|
|
169
|
+
cursor = labelEnd;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
throw new Error('Invalid DNS name');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function decodeIPv6Address(buffer, offset) {
|
|
176
|
+
const segments = [];
|
|
177
|
+
|
|
178
|
+
for (let index = 0; index < 8; index++) {
|
|
179
|
+
segments.push(buffer.readUInt16BE(offset + index * 2).toString(16));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return segments.join(':');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function parseDnsAddresses(buffer, hostname) {
|
|
186
|
+
if (buffer.length < 12) {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const normalizedHostname = normalizeHostname(hostname);
|
|
191
|
+
const questionCount = buffer.readUInt16BE(4);
|
|
192
|
+
const answerCount = buffer.readUInt16BE(6);
|
|
193
|
+
const authorityCount = buffer.readUInt16BE(8);
|
|
194
|
+
const additionalCount = buffer.readUInt16BE(10);
|
|
195
|
+
const recordCount = answerCount + authorityCount + additionalCount;
|
|
196
|
+
const addresses = [];
|
|
197
|
+
let offset = 12;
|
|
198
|
+
|
|
199
|
+
for (let index = 0; index < questionCount; index++) {
|
|
200
|
+
const question = readDnsName(buffer, offset);
|
|
201
|
+
offset = question.offset + 4;
|
|
202
|
+
|
|
203
|
+
if (offset > buffer.length) {
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (let index = 0; index < recordCount; index++) {
|
|
209
|
+
const recordName = readDnsName(buffer, offset);
|
|
210
|
+
offset = recordName.offset;
|
|
211
|
+
|
|
212
|
+
if (offset + 10 > buffer.length) {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const type = buffer.readUInt16BE(offset);
|
|
217
|
+
const klass = buffer.readUInt16BE(offset + 2) & 0x7fff;
|
|
218
|
+
const dataLength = buffer.readUInt16BE(offset + 8);
|
|
219
|
+
const dataOffset = offset + 10;
|
|
220
|
+
const nextOffset = dataOffset + dataLength;
|
|
221
|
+
|
|
222
|
+
if (nextOffset > buffer.length) {
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (klass === DNS_CLASS_IN && normalizeHostname(recordName.name) === normalizedHostname) {
|
|
227
|
+
if (type === DNS_RECORD_TYPES.A && dataLength === 4) {
|
|
228
|
+
addresses.push({
|
|
229
|
+
address: Array.from(buffer.subarray(dataOffset, nextOffset)).join('.'),
|
|
230
|
+
family: 4,
|
|
231
|
+
});
|
|
232
|
+
} else if (type === DNS_RECORD_TYPES.AAAA && dataLength === 16) {
|
|
233
|
+
addresses.push({
|
|
234
|
+
address: decodeIPv6Address(buffer, dataOffset),
|
|
235
|
+
family: 6,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
offset = nextOffset;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return addresses;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function selectLookupResult(addresses, options) {
|
|
247
|
+
const family = Number(options?.family) || 0;
|
|
248
|
+
let candidates = addresses;
|
|
249
|
+
|
|
250
|
+
if (family === 4 || family === 6) {
|
|
251
|
+
candidates = addresses.filter((address) => address.family === family);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!candidates.length) {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (options?.all === true) {
|
|
259
|
+
return {
|
|
260
|
+
address: candidates.map((candidate) => ({
|
|
261
|
+
address: candidate.address,
|
|
262
|
+
family: candidate.family,
|
|
263
|
+
})),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return candidates[0];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function createResolverLookup(signal) {
|
|
271
|
+
const dns = getDnsModule();
|
|
272
|
+
const net = getNetModule();
|
|
273
|
+
|
|
274
|
+
return (hostname, options, callback) => {
|
|
275
|
+
let settled = false;
|
|
276
|
+
let handleAbort = null;
|
|
277
|
+
const resolver = new dns.Resolver();
|
|
278
|
+
|
|
279
|
+
const finish = (error, address, family) => {
|
|
280
|
+
if (settled) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
settled = true;
|
|
285
|
+
|
|
286
|
+
if (signal && handleAbort) {
|
|
287
|
+
signal.removeEventListener('abort', handleAbort);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
callback(error, address, family);
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const ipFamily = net.isIP(hostname);
|
|
294
|
+
|
|
295
|
+
if (ipFamily) {
|
|
296
|
+
finish(null, hostname, ipFamily);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (signal) {
|
|
301
|
+
if (signal.aborted) {
|
|
302
|
+
finish(createAbortError(signal.reason));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
handleAbort = () => {
|
|
307
|
+
resolver.cancel();
|
|
308
|
+
finish(createAbortError(signal.reason));
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
signal.addEventListener('abort', handleAbort, { once: true });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const family = Number(options?.family) || 0;
|
|
315
|
+
|
|
316
|
+
if (family === 6) {
|
|
317
|
+
resolver.resolve6(hostname, (error, addresses) => {
|
|
318
|
+
if (error) {
|
|
319
|
+
finish(error);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (!Array.isArray(addresses) || addresses.length === 0) {
|
|
324
|
+
finish(new Error(`No DNS results for ${hostname}`));
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (options?.all === true) {
|
|
329
|
+
finish(
|
|
330
|
+
null,
|
|
331
|
+
addresses.map((address) => ({
|
|
332
|
+
address,
|
|
333
|
+
family: 6,
|
|
334
|
+
})),
|
|
335
|
+
);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
finish(null, addresses[0], 6);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
resolver.resolve4(hostname, (resolve4Error, addresses) => {
|
|
346
|
+
if (settled) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (!resolve4Error && Array.isArray(addresses) && addresses.length > 0) {
|
|
351
|
+
if (options?.all === true) {
|
|
352
|
+
finish(
|
|
353
|
+
null,
|
|
354
|
+
addresses.map((address) => ({
|
|
355
|
+
address,
|
|
356
|
+
family: 4,
|
|
357
|
+
})),
|
|
358
|
+
);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
finish(null, addresses[0], 4);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
resolver.resolve6(hostname, (resolve6Error, resolve6Addresses) => {
|
|
367
|
+
if (resolve6Error) {
|
|
368
|
+
finish(resolve4Error || resolve6Error);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (!Array.isArray(resolve6Addresses) || resolve6Addresses.length === 0) {
|
|
373
|
+
finish(resolve4Error || new Error(`No DNS results for ${hostname}`));
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (options?.all === true) {
|
|
378
|
+
finish(
|
|
379
|
+
null,
|
|
380
|
+
resolve6Addresses.map((address) => ({
|
|
381
|
+
address,
|
|
382
|
+
family: 6,
|
|
383
|
+
})),
|
|
384
|
+
);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
finish(null, resolve6Addresses[0], 6);
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function createMdnsLookup(signal) {
|
|
395
|
+
const dgram = getDgramModule();
|
|
396
|
+
const net = getNetModule();
|
|
397
|
+
|
|
398
|
+
return (hostname, options, callback) => {
|
|
399
|
+
let settled = false;
|
|
400
|
+
let handleAbort = null;
|
|
401
|
+
let sendTimers = [];
|
|
402
|
+
const mdnsConfig = getMdnsConfig();
|
|
403
|
+
const socket = mdnsConfig.joinMulticast
|
|
404
|
+
? dgram.createSocket({ type: 'udp4', reuseAddr: true })
|
|
405
|
+
: dgram.createSocket('udp4');
|
|
406
|
+
const query = buildMdnsQuery(hostname, options);
|
|
407
|
+
|
|
408
|
+
const cleanup = () => {
|
|
409
|
+
sendTimers.forEach(clearTimeout);
|
|
410
|
+
sendTimers = [];
|
|
411
|
+
|
|
412
|
+
if (signal && handleAbort) {
|
|
413
|
+
signal.removeEventListener('abort', handleAbort);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
socket.removeAllListeners('error');
|
|
417
|
+
socket.removeAllListeners('message');
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
socket.close();
|
|
421
|
+
} catch (error) {
|
|
422
|
+
void error;
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const finish = (error, address, family) => {
|
|
427
|
+
if (settled) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
settled = true;
|
|
432
|
+
cleanup();
|
|
433
|
+
callback(error, address, family);
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const ipFamily = net.isIP(hostname);
|
|
437
|
+
|
|
438
|
+
if (ipFamily) {
|
|
439
|
+
finish(null, hostname, ipFamily);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (signal) {
|
|
444
|
+
if (signal.aborted) {
|
|
445
|
+
finish(createAbortError(signal.reason));
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
handleAbort = () => {
|
|
450
|
+
finish(createAbortError(signal.reason));
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
signal.addEventListener('abort', handleAbort, { once: true });
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
socket.on('error', (error) => {
|
|
457
|
+
finish(error);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
socket.on('message', (message) => {
|
|
461
|
+
let addresses = [];
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
addresses = parseDnsAddresses(message, hostname);
|
|
465
|
+
} catch (error) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const result = selectLookupResult(addresses, options);
|
|
470
|
+
|
|
471
|
+
if (!result) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (options?.all === true) {
|
|
476
|
+
finish(null, result.address);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
finish(null, result.address, result.family);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
socket.bind(mdnsConfig.joinMulticast ? mdnsConfig.port : 0, () => {
|
|
484
|
+
if (settled) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (mdnsConfig.joinMulticast) {
|
|
489
|
+
try {
|
|
490
|
+
socket.addMembership(mdnsConfig.host);
|
|
491
|
+
} catch (error) {
|
|
492
|
+
void error;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const sendQuery = () => {
|
|
497
|
+
if (settled) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
socket.send(query, mdnsConfig.port, mdnsConfig.host, (error) => {
|
|
502
|
+
if (error) {
|
|
503
|
+
finish(error);
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
sendQuery();
|
|
509
|
+
sendTimers = [250, 1000].map((delay) => setTimeout(sendQuery, delay));
|
|
510
|
+
});
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function getLookup(url, signal) {
|
|
515
|
+
const hostname = getHostname(url);
|
|
516
|
+
|
|
517
|
+
if (typeof hostname !== 'string') {
|
|
518
|
+
return undefined;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (shouldUseResolverLookupForHostname(hostname)) {
|
|
522
|
+
return createResolverLookup(signal);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (shouldUseMdnsLookupForHostname(hostname)) {
|
|
526
|
+
return createMdnsLookup(signal);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return undefined;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function getAgent(url, signal) {
|
|
533
|
+
const { http, https } = getNodeHttpModules();
|
|
534
|
+
const lookup = getLookup(url, signal);
|
|
535
|
+
|
|
536
|
+
if (!lookup) {
|
|
537
|
+
return undefined;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (typeof url !== 'string') {
|
|
541
|
+
return undefined;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (url.startsWith('https://')) {
|
|
545
|
+
return new https.Agent({
|
|
546
|
+
keepAlive: false,
|
|
547
|
+
lookup,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (url.startsWith('http://')) {
|
|
552
|
+
return new http.Agent({
|
|
553
|
+
keepAlive: false,
|
|
554
|
+
lookup,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return undefined;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
class DiscoveryNodeTransport {
|
|
562
|
+
static shouldUseForUrl(url) {
|
|
563
|
+
const hostname = getHostname(url);
|
|
564
|
+
|
|
565
|
+
return (
|
|
566
|
+
typeof hostname === 'string' &&
|
|
567
|
+
(
|
|
568
|
+
shouldUseResolverLookupForHostname(hostname) ||
|
|
569
|
+
shouldUseMdnsLookupForHostname(hostname)
|
|
570
|
+
)
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
static async fetch(url, options, timeoutDuration, timeoutMessage) {
|
|
575
|
+
return Util.fetch(
|
|
576
|
+
url,
|
|
577
|
+
options,
|
|
578
|
+
timeoutDuration,
|
|
579
|
+
timeoutMessage,
|
|
580
|
+
(resolvedOptions) => {
|
|
581
|
+
if (typeof resolvedOptions.agent === 'undefined') {
|
|
582
|
+
resolvedOptions.agent = getAgent(url, resolvedOptions.signal);
|
|
583
|
+
}
|
|
584
|
+
},
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
module.exports = DiscoveryNodeTransport;
|
|
@@ -483,10 +483,16 @@ class HomeyAPIV3 extends HomeyAPI {
|
|
|
483
483
|
.finally(() => {
|
|
484
484
|
// Delete after 30 seconds some requests might still be pending and they should be able
|
|
485
485
|
// to receive a rejected promise for this token.
|
|
486
|
-
|
|
486
|
+
const cleanupTimer = setTimeout(() => {
|
|
487
487
|
delete this.__refreshMap[token];
|
|
488
488
|
delete this.__refreshMap[token + 'timeout'];
|
|
489
489
|
}, 30 * 1000);
|
|
490
|
+
|
|
491
|
+
if (typeof cleanupTimer.unref === 'function') {
|
|
492
|
+
cleanupTimer.unref();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
this.__refreshMap[token + 'timeout'] = cleanupTimer;
|
|
490
496
|
});
|
|
491
497
|
}
|
|
492
498
|
|
|
@@ -536,6 +542,14 @@ class HomeyAPIV3 extends HomeyAPI {
|
|
|
536
542
|
|
|
537
543
|
destroy() {
|
|
538
544
|
this.__destroyed = true;
|
|
545
|
+
|
|
546
|
+
for (const [key, value] of Object.entries(this.__refreshMap)) {
|
|
547
|
+
if (key.endsWith('timeout')) {
|
|
548
|
+
clearTimeout(value);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
this.__refreshMap = {};
|
|
539
553
|
this.__subscriptionRegistry.destroy();
|
|
540
554
|
this.__socketSession.destroy();
|
|
541
555
|
|
package/lib/Util.js
CHANGED
|
@@ -2,244 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
const APIErrorTimeout = require('./APIErrorTimeout');
|
|
4
4
|
|
|
5
|
-
let httpAgent = null;
|
|
6
|
-
let httpsAgent = null;
|
|
7
|
-
|
|
8
|
-
function getNodeHttpModules() {
|
|
9
|
-
return {
|
|
10
|
-
http: require('node:http'),
|
|
11
|
-
https: require('node:https'),
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function getDnsModule() {
|
|
16
|
-
return require('node:dns');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function getNetModule() {
|
|
20
|
-
return require('node:net');
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function getSharedNodeFetchAgents() {
|
|
24
|
-
const { http, https } = getNodeHttpModules();
|
|
25
|
-
|
|
26
|
-
httpAgent = httpAgent || new http.Agent({
|
|
27
|
-
keepAlive: false,
|
|
28
|
-
});
|
|
29
|
-
httpsAgent = httpsAgent || new https.Agent({
|
|
30
|
-
keepAlive: false,
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
httpAgent,
|
|
35
|
-
httpsAgent,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function getNodeFetchAgent(url) {
|
|
40
|
-
const { httpAgent, httpsAgent } = getSharedNodeFetchAgents();
|
|
41
|
-
|
|
42
|
-
if (typeof url !== 'string') {
|
|
43
|
-
return undefined;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (url.startsWith('https://')) {
|
|
47
|
-
return httpsAgent;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (url.startsWith('http://')) {
|
|
51
|
-
return httpAgent;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return undefined;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function createAbortError(reason) {
|
|
58
|
-
const error = new Error(
|
|
59
|
-
typeof reason === 'string' && reason.length > 0
|
|
60
|
-
? reason
|
|
61
|
-
: 'The operation was aborted',
|
|
62
|
-
);
|
|
63
|
-
error.name = 'AbortError';
|
|
64
|
-
error.code = 'ABORT_ERR';
|
|
65
|
-
error.type = 'aborted';
|
|
66
|
-
return error;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function shouldUseCancellableResolverLookup(hostname) {
|
|
70
|
-
if (typeof hostname !== 'string') {
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return hostname.endsWith('.homey.homeylocal.com');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function resolveHostnameWithResolver({ dns, hostname, options, finish }) {
|
|
78
|
-
const resolver = new dns.Resolver();
|
|
79
|
-
const family = Number(options?.family) || 0;
|
|
80
|
-
const wantsAll = options?.all === true;
|
|
81
|
-
|
|
82
|
-
const resolve4 = (callback) => resolver.resolve4(hostname, callback);
|
|
83
|
-
const resolve6 = (callback) => resolver.resolve6(hostname, callback);
|
|
84
|
-
|
|
85
|
-
const finishWithAddresses = (addresses, addressFamily) => {
|
|
86
|
-
if (!Array.isArray(addresses) || addresses.length === 0) {
|
|
87
|
-
finish(new Error(`No DNS results for ${hostname}`));
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (wantsAll) {
|
|
92
|
-
finish(
|
|
93
|
-
null,
|
|
94
|
-
addresses.map((address) => ({
|
|
95
|
-
address,
|
|
96
|
-
family: addressFamily,
|
|
97
|
-
})),
|
|
98
|
-
);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
finish(null, addresses[0], addressFamily);
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const maybeResolve6After4 = (error) => {
|
|
106
|
-
if (family === 4) {
|
|
107
|
-
finish(error);
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
resolve6((resolve6Error, addresses) => {
|
|
112
|
-
if (resolve6Error) {
|
|
113
|
-
finish(error || resolve6Error);
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
finishWithAddresses(addresses, 6);
|
|
118
|
-
});
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
if (family === 6) {
|
|
122
|
-
resolve6((error, addresses) => {
|
|
123
|
-
if (error) {
|
|
124
|
-
finish(error);
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
finishWithAddresses(addresses, 6);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
return () => resolver.cancel();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
resolve4((error, addresses) => {
|
|
135
|
-
if (error) {
|
|
136
|
-
maybeResolve6After4(error);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
finishWithAddresses(addresses, 4);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
return () => resolver.cancel();
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function createTimedLookup(timeoutDuration, signal) {
|
|
147
|
-
const dns = getDnsModule();
|
|
148
|
-
const net = getNetModule();
|
|
149
|
-
|
|
150
|
-
return (hostname, options, callback) => {
|
|
151
|
-
let settled = false;
|
|
152
|
-
let timer = null;
|
|
153
|
-
let handleAbort = null;
|
|
154
|
-
let cancelLookup = null;
|
|
155
|
-
|
|
156
|
-
const finish = (error, address, family) => {
|
|
157
|
-
if (settled) {
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
settled = true;
|
|
162
|
-
clearTimeout(timer);
|
|
163
|
-
|
|
164
|
-
if (signal && handleAbort) {
|
|
165
|
-
signal.removeEventListener('abort', handleAbort);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (cancelLookup) {
|
|
169
|
-
cancelLookup();
|
|
170
|
-
cancelLookup = null;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
callback(error, address, family);
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
const ipFamily = net.isIP(hostname);
|
|
177
|
-
|
|
178
|
-
if (ipFamily) {
|
|
179
|
-
finish(null, hostname, ipFamily);
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (signal) {
|
|
184
|
-
if (signal.aborted) {
|
|
185
|
-
finish(createAbortError(signal.reason));
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
handleAbort = () => {
|
|
190
|
-
finish(createAbortError(signal.reason));
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
signal.addEventListener('abort', handleAbort, { once: true });
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
timer = setTimeout(() => {
|
|
197
|
-
const error = new APIErrorTimeout(`DNS lookup timeout after ${timeoutDuration}ms`);
|
|
198
|
-
error.code = 'ETIMEDOUT';
|
|
199
|
-
error.syscall = 'getaddrinfo';
|
|
200
|
-
error.stage = 'dns-lookup';
|
|
201
|
-
finish(error);
|
|
202
|
-
}, timeoutDuration);
|
|
203
|
-
|
|
204
|
-
if (shouldUseCancellableResolverLookup(hostname)) {
|
|
205
|
-
cancelLookup = resolveHostnameWithResolver({
|
|
206
|
-
dns,
|
|
207
|
-
hostname,
|
|
208
|
-
options,
|
|
209
|
-
finish,
|
|
210
|
-
});
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
dns.lookup(hostname, options, finish);
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function getTimedNodeFetchAgent(url, timeoutDuration, signal) {
|
|
219
|
-
const { http, https } = getNodeHttpModules();
|
|
220
|
-
const lookup = createTimedLookup(timeoutDuration, signal);
|
|
221
|
-
|
|
222
|
-
if (typeof url !== 'string') {
|
|
223
|
-
return undefined;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (url.startsWith('https://')) {
|
|
227
|
-
return new https.Agent({
|
|
228
|
-
keepAlive: false,
|
|
229
|
-
lookup,
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (url.startsWith('http://')) {
|
|
234
|
-
return new http.Agent({
|
|
235
|
-
keepAlive: false,
|
|
236
|
-
lookup,
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return undefined;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
5
|
/**
|
|
244
6
|
* Helper Utility Class
|
|
245
7
|
* @class
|
|
@@ -253,9 +15,10 @@ class Util {
|
|
|
253
15
|
* @param {{}=} options
|
|
254
16
|
* @param {number=} timeoutDuration
|
|
255
17
|
* @param {string=} timeoutMessage
|
|
18
|
+
* @param {Function=} patchOptions
|
|
256
19
|
* @returns {Promise<any>}
|
|
257
20
|
*/
|
|
258
|
-
static async fetch(url, options, timeoutDuration, timeoutMessage) {
|
|
21
|
+
static async fetch(url, options, timeoutDuration, timeoutMessage, patchOptions) {
|
|
259
22
|
options = { ...options };
|
|
260
23
|
let timeoutTimer = null;
|
|
261
24
|
let composedAbortController = null;
|
|
@@ -274,11 +37,7 @@ class Util {
|
|
|
274
37
|
handleExternalAbort = () => {
|
|
275
38
|
composedAbortController.abort(externalSignal.reason);
|
|
276
39
|
};
|
|
277
|
-
externalSignal.addEventListener(
|
|
278
|
-
'abort',
|
|
279
|
-
handleExternalAbort,
|
|
280
|
-
{ once: true }
|
|
281
|
-
);
|
|
40
|
+
externalSignal.addEventListener('abort', handleExternalAbort, { once: true });
|
|
282
41
|
}
|
|
283
42
|
}
|
|
284
43
|
|
|
@@ -294,6 +53,14 @@ class Util {
|
|
|
294
53
|
options.signal = composedAbortController.signal;
|
|
295
54
|
}
|
|
296
55
|
|
|
56
|
+
if (typeof patchOptions === 'function') {
|
|
57
|
+
const patchedOptions = patchOptions(options, url);
|
|
58
|
+
|
|
59
|
+
if (patchedOptions && typeof patchedOptions === 'object') {
|
|
60
|
+
options = patchedOptions;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
297
64
|
let responsePromise = null;
|
|
298
65
|
|
|
299
66
|
if (this.isReactNative()) {
|
|
@@ -302,13 +69,6 @@ class Util {
|
|
|
302
69
|
responsePromise = window.fetch(url, options);
|
|
303
70
|
} else if (this.isNodeJS()) {
|
|
304
71
|
const fetch = require('node-fetch');
|
|
305
|
-
|
|
306
|
-
if (typeof options.agent === 'undefined') {
|
|
307
|
-
options.agent = timeoutDuration != null
|
|
308
|
-
? getTimedNodeFetchAgent(url, timeoutDuration, options.signal)
|
|
309
|
-
: getNodeFetchAgent(url);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
72
|
responsePromise = fetch(url, options);
|
|
313
73
|
} else if (typeof fetch !== 'undefined') {
|
|
314
74
|
responsePromise = fetch(url, options);
|
|
@@ -354,7 +114,7 @@ class Util {
|
|
|
354
114
|
static async timeout(
|
|
355
115
|
promise,
|
|
356
116
|
timeoutMillis = 5000,
|
|
357
|
-
message = `Timeout after ${timeoutMillis}ms
|
|
117
|
+
message = `Timeout after ${timeoutMillis}ms`,
|
|
358
118
|
) {
|
|
359
119
|
const timeoutError = new APIErrorTimeout(message);
|
|
360
120
|
let timeoutRef;
|
|
@@ -370,7 +130,7 @@ class Util {
|
|
|
370
130
|
|
|
371
131
|
returnPromise
|
|
372
132
|
// eslint-disable-next-line no-unused-vars
|
|
373
|
-
.catch((err) => {
|
|
133
|
+
.catch((err) => {})
|
|
374
134
|
.finally(() => {
|
|
375
135
|
clearTimeout(timeoutRef);
|
|
376
136
|
});
|
|
@@ -421,7 +181,12 @@ class Util {
|
|
|
421
181
|
*/
|
|
422
182
|
static isNodeJS() {
|
|
423
183
|
if (this.isReactNative()) return false;
|
|
424
|
-
return
|
|
184
|
+
return (
|
|
185
|
+
typeof process !== 'undefined' &&
|
|
186
|
+
typeof process.versions === 'object' &&
|
|
187
|
+
process.versions !== null &&
|
|
188
|
+
typeof process.versions.node === 'string'
|
|
189
|
+
);
|
|
425
190
|
}
|
|
426
191
|
|
|
427
192
|
/**
|
|
@@ -560,7 +325,7 @@ class Util {
|
|
|
560
325
|
buildParams(
|
|
561
326
|
prefix + '[' + (typeof obj[index] === 'object' ? index : '') + ']',
|
|
562
327
|
obj[index],
|
|
563
|
-
add
|
|
328
|
+
add,
|
|
564
329
|
);
|
|
565
330
|
}
|
|
566
331
|
}
|
|
@@ -603,7 +368,6 @@ class Util {
|
|
|
603
368
|
|
|
604
369
|
return encodedPairs.join('&');
|
|
605
370
|
}
|
|
606
|
-
|
|
607
371
|
}
|
|
608
372
|
|
|
609
373
|
module.exports = Util;
|