zgrzyt 2.1.1 → 2.2.1
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/Readme.md +2 -1
- package/lib/check.js +33 -15
- package/lib/cloudflare.js +48 -16
- package/lib/config.js +4 -0
- package/lib/dns.js +32 -6
- package/package.json +2 -3
package/Readme.md
CHANGED
|
@@ -36,6 +36,7 @@ zgrzyt --force
|
|
|
36
36
|
; list of potential servers configured for this domain CNAME record
|
|
37
37
|
servers[]=alfa.example.net
|
|
38
38
|
servers[]=beta.example.net
|
|
39
|
+
ipv6=true ; by default ipv4 is true and ipv6 is false
|
|
39
40
|
|
|
40
41
|
[cloudflare]
|
|
41
42
|
; cloudflare API token
|
|
@@ -51,7 +52,7 @@ method=HEAD ; optional, HTTP method used by zgrzyt
|
|
|
51
52
|
```
|
|
52
53
|
|
|
53
54
|
If `api.domain` is not specified its value is deduced from `api.url`.
|
|
54
|
-
If `api.method` is not specified zgrzyt will send `HEAD` requests to poll the servers. `api.method` can be set to `GET` if needed. `retry` and `
|
|
55
|
+
If `api.method` is not specified zgrzyt will send `HEAD` requests to poll the servers. `api.method` can be set to `GET` if needed. `retry`, `timeout`, `ipv4`, and `ipv6` can be either configured globally or separately for each API.
|
|
55
56
|
|
|
56
57
|
### Multiple checks
|
|
57
58
|
|
package/lib/check.js
CHANGED
|
@@ -23,25 +23,39 @@ export {
|
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
async function checkService(server, api) {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
const { ipv4 = true, ipv6 = false } = api;
|
|
27
|
+
const addresses = await resolve(server, { ipv4, ipv6 });
|
|
28
|
+
if (debug.enabled) {
|
|
29
|
+
debug('Resolved %s to %s', server, addresses.map(a => a.address).join(', '));
|
|
30
|
+
}
|
|
31
|
+
const oks = await Promise.all(addresses.map(a => checkApi(api, a)));
|
|
32
|
+
// collate results
|
|
33
|
+
const ok = oks.every(ok => ok);
|
|
29
34
|
const health = await updateHealth(api, server, ok);
|
|
30
35
|
return {
|
|
31
|
-
address,
|
|
36
|
+
address: addresses[0].address,
|
|
32
37
|
server,
|
|
33
38
|
health,
|
|
34
39
|
ok
|
|
35
40
|
};
|
|
36
41
|
}
|
|
37
42
|
|
|
38
|
-
async function checkApi(
|
|
43
|
+
async function checkApi(api, { address, family }) {
|
|
44
|
+
|
|
45
|
+
const {
|
|
46
|
+
url,
|
|
47
|
+
method = 'HEAD',
|
|
48
|
+
timeout = 5000,
|
|
49
|
+
headers = {}
|
|
50
|
+
} = api;
|
|
51
|
+
|
|
39
52
|
debug('Checking %s on %s with timeout %dms', url, address, timeout);
|
|
40
53
|
|
|
41
54
|
const { protocol } = new URL(url);
|
|
42
55
|
const { request } = 'https:' === protocol ? https : http;
|
|
43
56
|
|
|
44
57
|
let result;
|
|
58
|
+
let retry = api.retry || 3;
|
|
45
59
|
do {
|
|
46
60
|
result = await makeRequest();
|
|
47
61
|
if (result) {
|
|
@@ -54,15 +68,15 @@ async function checkApi({ url, timeout, method = 'HEAD', headers = {}, retry = 2
|
|
|
54
68
|
|
|
55
69
|
function makeRequest() {
|
|
56
70
|
return new Promise(resolve => request(url, {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
71
|
+
method,
|
|
72
|
+
family,
|
|
73
|
+
lookup,
|
|
74
|
+
timeout,
|
|
75
|
+
headers: {
|
|
76
|
+
'User-Agent': USER_AGENT,
|
|
77
|
+
...headers
|
|
78
|
+
}
|
|
79
|
+
})
|
|
66
80
|
.on('timeout', function () {
|
|
67
81
|
debug('Timeout for %s on %s', url, address);
|
|
68
82
|
this.destroy(new Error('Request timeout.'));
|
|
@@ -82,7 +96,11 @@ async function checkApi({ url, timeout, method = 'HEAD', headers = {}, retry = 2
|
|
|
82
96
|
}
|
|
83
97
|
|
|
84
98
|
function lookup(domain, options, fn) {
|
|
85
|
-
|
|
99
|
+
debug('lookup', domain, address, options);
|
|
100
|
+
if (options.all) {
|
|
101
|
+
address = [address];
|
|
102
|
+
}
|
|
103
|
+
setImmediate(fn, null, address, options.family);
|
|
86
104
|
}
|
|
87
105
|
}
|
|
88
106
|
|
package/lib/cloudflare.js
CHANGED
|
@@ -1,29 +1,66 @@
|
|
|
1
|
-
import got from 'got';
|
|
2
1
|
import makeDebug from 'debug';
|
|
3
2
|
|
|
4
3
|
const debug = makeDebug('zgrzyt:cloudflare');
|
|
5
4
|
|
|
6
5
|
export default client;
|
|
7
6
|
|
|
7
|
+
/* global AbortController, fetch, URLSearchParams */
|
|
8
|
+
|
|
8
9
|
class CloudflareError extends Error {
|
|
9
10
|
constructor(errors) {
|
|
10
11
|
super(`Cloudflare API error: ${errors[0].message}`);
|
|
11
12
|
}
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
function
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
function makeFetch({ token, timeout, retry }) {
|
|
16
|
+
const authorization = `Bearer ${token}`;
|
|
17
|
+
return {
|
|
18
|
+
put: (...args) => retryFetch('PUT', ...args),
|
|
19
|
+
get: (...args) => retryFetch('GET', ...args)
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
async function retryFetch(...args) {
|
|
23
|
+
while (true) {
|
|
24
|
+
try {
|
|
25
|
+
return await doFetch(...args);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if (--retry <= 0) {
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function doFetch(method, command, { searchParams, json }) {
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
const id = setTimeout(() => controller.abort(), timeout);
|
|
37
|
+
const url = new URL(command, 'https://api.cloudflare.com/client/v4/');
|
|
38
|
+
if (searchParams) {
|
|
39
|
+
url.search = new URLSearchParams(searchParams);
|
|
40
|
+
}
|
|
41
|
+
const options = {
|
|
42
|
+
method,
|
|
43
|
+
headers: { authorization },
|
|
44
|
+
signal: controller.signal
|
|
45
|
+
};
|
|
46
|
+
if (json) {
|
|
47
|
+
options.headers['Content-Type'] = 'application/json';
|
|
48
|
+
options.body = JSON.stringify(json);
|
|
23
49
|
}
|
|
24
|
-
|
|
50
|
+
const res = await fetch(url, options);
|
|
51
|
+
clearTimeout(id);
|
|
52
|
+
return res.json();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function client({ token, timeout = 4000, retry = 2 }) {
|
|
57
|
+
const cf = makeFetch({ token, timeout, retry });
|
|
25
58
|
const cacheZones = Object.create(null);
|
|
26
59
|
let pZones;
|
|
60
|
+
return {
|
|
61
|
+
switchToService,
|
|
62
|
+
listRecords
|
|
63
|
+
};
|
|
27
64
|
|
|
28
65
|
async function switchToService(zoneName, domain, good) {
|
|
29
66
|
const zone = await getZone(zoneName);
|
|
@@ -128,9 +165,4 @@ function client({ token, timeout = 4000, retry = 2 }) {
|
|
|
128
165
|
|
|
129
166
|
return success;
|
|
130
167
|
}
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
switchToService,
|
|
134
|
-
listRecords
|
|
135
|
-
};
|
|
136
168
|
}
|
package/lib/config.js
CHANGED
|
@@ -53,6 +53,8 @@ function prepareConfig(config) {
|
|
|
53
53
|
const retry = parseInt(api.retry || config.retry, 10) || 2;
|
|
54
54
|
const force = 'force' in api ? api.force : config.force;
|
|
55
55
|
const repair = parseInt(api.repair || config.repair, 10) || 5;
|
|
56
|
+
const ipv6 = Boolean(api.ipv6 ?? config.ipv6);
|
|
57
|
+
const ipv4 = Boolean(api.ipv4 ?? config.ipv4 ?? true);
|
|
56
58
|
|
|
57
59
|
const { servers } = api.cluster in cluster ? cluster[api.cluster] : config;
|
|
58
60
|
if (!servers || !servers.length) {
|
|
@@ -78,6 +80,8 @@ function prepareConfig(config) {
|
|
|
78
80
|
timeout,
|
|
79
81
|
retry,
|
|
80
82
|
domain,
|
|
83
|
+
ipv4,
|
|
84
|
+
ipv6,
|
|
81
85
|
zone
|
|
82
86
|
},
|
|
83
87
|
client,
|
package/lib/dns.js
CHANGED
|
@@ -1,22 +1,48 @@
|
|
|
1
1
|
import { Resolver } from 'node:dns/promises';
|
|
2
2
|
import makeDebug from 'debug';
|
|
3
3
|
|
|
4
|
-
const resolver = new Resolver();
|
|
5
4
|
const debug = makeDebug('dns:zgrzyt');
|
|
6
5
|
|
|
7
6
|
const cache = Object.create(null);
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
const resolver = new Resolver();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolve domain to IP addresses
|
|
12
|
+
*
|
|
13
|
+
* @param {String} domain
|
|
14
|
+
* @param { ipv4, ipv6 } type of addresses to resolve
|
|
15
|
+
* @returns {Promise<Array<{address: String, family: String}>>} list of resolved addresses
|
|
16
|
+
*/
|
|
17
|
+
export async function resolve(domain, { ipv4, ipv6 }) {
|
|
10
18
|
let p = cache[domain];
|
|
11
19
|
if (!p) {
|
|
12
20
|
cache[domain] = p = doResolve(domain);
|
|
13
21
|
}
|
|
14
|
-
|
|
22
|
+
const addresses = await p;
|
|
23
|
+
return addresses.filter(
|
|
24
|
+
a => (a.family === 4 && ipv4) || (a.family === 6 && ipv6)
|
|
25
|
+
);
|
|
15
26
|
}
|
|
16
27
|
|
|
28
|
+
const RRTYPE_TO_FAMILY = {
|
|
29
|
+
A: 4,
|
|
30
|
+
AAAA: 6
|
|
31
|
+
};
|
|
32
|
+
|
|
17
33
|
async function doResolve(domain) {
|
|
18
34
|
debug('Resolving %s', domain);
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
return
|
|
35
|
+
|
|
36
|
+
const addresses = await Promise.all(['A', 'AAAA'].map(resolveWithFamily));
|
|
37
|
+
return addresses.flat();
|
|
38
|
+
|
|
39
|
+
async function resolveWithFamily(rrtype) {
|
|
40
|
+
try {
|
|
41
|
+
const addresses = await resolver.resolve(domain, rrtype);
|
|
42
|
+
return addresses.map(address => ({ address, family: RRTYPE_TO_FAMILY[rrtype] }));
|
|
43
|
+
} catch (error) {
|
|
44
|
+
debug('Failed to resolve %s with type %s: %s', domain, rrtype, error);
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
22
48
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zgrzyt",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "Poor man's load balancing DNS switcher.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": {
|
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"debug": "~4",
|
|
30
|
-
"got": "~14",
|
|
31
30
|
"parse-domain": "~8",
|
|
32
31
|
"rc": "^1.2.8",
|
|
33
32
|
"sprintfjs": "^1.2.16"
|
|
@@ -43,4 +42,4 @@
|
|
|
43
42
|
"lib",
|
|
44
43
|
"bin"
|
|
45
44
|
]
|
|
46
|
-
}
|
|
45
|
+
}
|