zgrzyt 2.2.1 → 2.2.3
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/index.js +1 -1
- package/lib/check.js +30 -32
- package/lib/cloudflare.js +23 -28
- package/lib/config.js +7 -12
- package/lib/dns.js +4 -1
- package/lib/report.js +15 -8
- package/lib/state.js +3 -9
- package/lib/zgrzyt.js +4 -1
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import rc from 'rc';
|
|
2
2
|
import prepareConfig from './lib/config.js';
|
|
3
|
-
import { zgrzyt } from './lib/zgrzyt.js';
|
|
4
3
|
import { report } from './lib/report.js';
|
|
5
4
|
import { onExit } from './lib/state.js';
|
|
5
|
+
import { zgrzyt } from './lib/zgrzyt.js';
|
|
6
6
|
|
|
7
7
|
const conf = rc('zgrzyt');
|
|
8
8
|
const apis = prepareConfig(conf);
|
package/lib/check.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
1
|
import http from 'node:http';
|
|
3
2
|
import https from 'node:https';
|
|
4
3
|
import makeDebug from 'debug';
|
|
@@ -8,25 +7,28 @@ const debug = makeDebug('zgrzyt:check');
|
|
|
8
7
|
import { resolve } from './dns.js';
|
|
9
8
|
import { updateHealth } from './state.js';
|
|
10
9
|
|
|
10
|
+
import packageJson from '../package.json' with { type: 'json' };
|
|
11
11
|
const {
|
|
12
12
|
name,
|
|
13
13
|
version,
|
|
14
14
|
homepage = 'https://github.com/pirxpilot/zgrzyt'
|
|
15
|
-
} =
|
|
15
|
+
} = packageJson;
|
|
16
16
|
|
|
17
17
|
const USER_AGENT = `${name}/${version} (${homepage})`;
|
|
18
18
|
|
|
19
19
|
/* global URL */
|
|
20
20
|
|
|
21
|
-
export {
|
|
22
|
-
checkServices
|
|
23
|
-
};
|
|
21
|
+
export { checkServices };
|
|
24
22
|
|
|
25
23
|
async function checkService(server, api) {
|
|
26
24
|
const { ipv4 = true, ipv6 = false } = api;
|
|
27
25
|
const addresses = await resolve(server, { ipv4, ipv6 });
|
|
28
26
|
if (debug.enabled) {
|
|
29
|
-
debug(
|
|
27
|
+
debug(
|
|
28
|
+
'Resolved %s to %s',
|
|
29
|
+
server,
|
|
30
|
+
addresses.map(a => a.address).join(', ')
|
|
31
|
+
);
|
|
30
32
|
}
|
|
31
33
|
const oks = await Promise.all(addresses.map(a => checkApi(api, a)));
|
|
32
34
|
// collate results
|
|
@@ -41,13 +43,7 @@ async function checkService(server, api) {
|
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
async function checkApi(api, { address, family }) {
|
|
44
|
-
|
|
45
|
-
const {
|
|
46
|
-
url,
|
|
47
|
-
method = 'HEAD',
|
|
48
|
-
timeout = 5000,
|
|
49
|
-
headers = {}
|
|
50
|
-
} = api;
|
|
46
|
+
const { url, method = 'HEAD', timeout = 5000, headers = {} } = api;
|
|
51
47
|
|
|
52
48
|
debug('Checking %s on %s with timeout %dms', url, address, timeout);
|
|
53
49
|
|
|
@@ -67,26 +63,28 @@ async function checkApi(api, { address, family }) {
|
|
|
67
63
|
return result;
|
|
68
64
|
|
|
69
65
|
function makeRequest() {
|
|
70
|
-
return new Promise(resolve =>
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
.on('timeout', function () {
|
|
81
|
-
debug('Timeout for %s on %s', url, address);
|
|
82
|
-
this.destroy(new Error('Request timeout.'));
|
|
83
|
-
})
|
|
84
|
-
.on('response', res => resolve(isOk(res)))
|
|
85
|
-
.on('error', e => {
|
|
86
|
-
debug('request error:', e);
|
|
87
|
-
resolve(false);
|
|
66
|
+
return new Promise(resolve =>
|
|
67
|
+
request(url, {
|
|
68
|
+
method,
|
|
69
|
+
family,
|
|
70
|
+
lookup,
|
|
71
|
+
timeout,
|
|
72
|
+
headers: {
|
|
73
|
+
'User-Agent': USER_AGENT,
|
|
74
|
+
...headers
|
|
75
|
+
}
|
|
88
76
|
})
|
|
89
|
-
|
|
77
|
+
.on('timeout', function () {
|
|
78
|
+
debug('Timeout for %s on %s', url, address);
|
|
79
|
+
this.destroy(new Error('Request timeout.'));
|
|
80
|
+
})
|
|
81
|
+
.on('response', res => resolve(isOk(res)))
|
|
82
|
+
.on('error', e => {
|
|
83
|
+
debug('request error:', e);
|
|
84
|
+
resolve(false);
|
|
85
|
+
})
|
|
86
|
+
.end()
|
|
87
|
+
);
|
|
90
88
|
|
|
91
89
|
function isOk({ statusCode }) {
|
|
92
90
|
const ok = statusCode >= 200 && statusCode < 300;
|
package/lib/cloudflare.js
CHANGED
|
@@ -4,8 +4,6 @@ const debug = makeDebug('zgrzyt:cloudflare');
|
|
|
4
4
|
|
|
5
5
|
export default client;
|
|
6
6
|
|
|
7
|
-
/* global AbortController, fetch, URLSearchParams */
|
|
8
|
-
|
|
9
7
|
class CloudflareError extends Error {
|
|
10
8
|
constructor(errors) {
|
|
11
9
|
super(`Cloudflare API error: ${errors[0].message}`);
|
|
@@ -32,8 +30,6 @@ function makeFetch({ token, timeout, retry }) {
|
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
async function doFetch(method, command, { searchParams, json }) {
|
|
35
|
-
const controller = new AbortController();
|
|
36
|
-
const id = setTimeout(() => controller.abort(), timeout);
|
|
37
33
|
const url = new URL(command, 'https://api.cloudflare.com/client/v4/');
|
|
38
34
|
if (searchParams) {
|
|
39
35
|
url.search = new URLSearchParams(searchParams);
|
|
@@ -41,14 +37,13 @@ function makeFetch({ token, timeout, retry }) {
|
|
|
41
37
|
const options = {
|
|
42
38
|
method,
|
|
43
39
|
headers: { authorization },
|
|
44
|
-
signal:
|
|
40
|
+
signal: AbortSignal.timeout(timeout)
|
|
45
41
|
};
|
|
46
42
|
if (json) {
|
|
47
43
|
options.headers['Content-Type'] = 'application/json';
|
|
48
44
|
options.body = JSON.stringify(json);
|
|
49
45
|
}
|
|
50
46
|
const res = await fetch(url, options);
|
|
51
|
-
clearTimeout(id);
|
|
52
47
|
return res.json();
|
|
53
48
|
}
|
|
54
49
|
}
|
|
@@ -67,9 +62,8 @@ function client({ token, timeout = 4000, retry = 2 }) {
|
|
|
67
62
|
const record = await getRecord(zone.id, domain);
|
|
68
63
|
if (record.type === 'CNAME') {
|
|
69
64
|
return updateRecord(zone.id, record.id, record.proxied, domain, good);
|
|
70
|
-
} else {
|
|
71
|
-
console.error('Cannot only update CNAME records');
|
|
72
65
|
}
|
|
66
|
+
console.error('Cannot only update CNAME records');
|
|
73
67
|
}
|
|
74
68
|
|
|
75
69
|
async function listRecords(zoneName, domain) {
|
|
@@ -100,12 +94,7 @@ function client({ token, timeout = 4000, retry = 2 }) {
|
|
|
100
94
|
|
|
101
95
|
async function getPage(page) {
|
|
102
96
|
debug('Getting page %d', page);
|
|
103
|
-
const {
|
|
104
|
-
success,
|
|
105
|
-
errors,
|
|
106
|
-
result,
|
|
107
|
-
result_info: { total_count }
|
|
108
|
-
} = await cf.get('zones', {
|
|
97
|
+
const { success, errors, result, result_info } = await cf.get('zones', {
|
|
109
98
|
searchParams: {
|
|
110
99
|
status: 'active',
|
|
111
100
|
page,
|
|
@@ -118,23 +107,26 @@ function client({ token, timeout = 4000, retry = 2 }) {
|
|
|
118
107
|
}
|
|
119
108
|
|
|
120
109
|
zones.push(...result);
|
|
121
|
-
return zones.length < total_count;
|
|
110
|
+
return zones.length < result_info.total_count;
|
|
122
111
|
}
|
|
123
112
|
|
|
124
113
|
debug('Listing zones...');
|
|
125
|
-
for (let page = 1; await getPage(page); page++) {
|
|
114
|
+
for (let page = 1; await getPage(page); page++) {}
|
|
126
115
|
debug('Listing zones done.');
|
|
127
116
|
|
|
128
117
|
return zones;
|
|
129
118
|
}
|
|
130
119
|
|
|
131
120
|
async function getRecord(zoneId, domain) {
|
|
132
|
-
const { success, result, errors } = await cf.get(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
121
|
+
const { success, result, errors } = await cf.get(
|
|
122
|
+
`zones/${zoneId}/dns_records`,
|
|
123
|
+
{
|
|
124
|
+
searchParams: {
|
|
125
|
+
name: domain,
|
|
126
|
+
type: 'CNAME'
|
|
127
|
+
}
|
|
136
128
|
}
|
|
137
|
-
|
|
129
|
+
);
|
|
138
130
|
|
|
139
131
|
if (!success) {
|
|
140
132
|
throw new CloudflareError(errors);
|
|
@@ -148,14 +140,17 @@ function client({ token, timeout = 4000, retry = 2 }) {
|
|
|
148
140
|
}
|
|
149
141
|
|
|
150
142
|
async function updateRecord(zoneId, recordId, proxied, domain, good) {
|
|
151
|
-
const { success, errors, result } = await cf.put(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
143
|
+
const { success, errors, result } = await cf.put(
|
|
144
|
+
`zones/${zoneId}/dns_records/${recordId}`,
|
|
145
|
+
{
|
|
146
|
+
json: {
|
|
147
|
+
name: domain,
|
|
148
|
+
proxied,
|
|
149
|
+
content: good.server,
|
|
150
|
+
type: 'CNAME'
|
|
151
|
+
}
|
|
157
152
|
}
|
|
158
|
-
|
|
153
|
+
);
|
|
159
154
|
|
|
160
155
|
debug('Update record', success, errors, result);
|
|
161
156
|
|
package/lib/config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fromUrl, parseDomain } from 'parse-domain';
|
|
2
2
|
import makeClient from './cloudflare.js';
|
|
3
3
|
|
|
4
4
|
export default prepareConfig;
|
|
@@ -15,21 +15,17 @@ function getDomainAndZone({ url, domain: apiDomain }) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function prepareConfig(config) {
|
|
18
|
-
const {
|
|
19
|
-
cloudflare,
|
|
20
|
-
api = {},
|
|
21
|
-
cluster = {}
|
|
22
|
-
} = config;
|
|
18
|
+
const { cloudflare, api = {}, cluster = {} } = config;
|
|
23
19
|
|
|
24
20
|
if (!cloudflare?.token) {
|
|
25
21
|
console.error('Cloudflare API token not configured');
|
|
26
22
|
return;
|
|
27
23
|
}
|
|
28
24
|
if (typeof cloudflare.timeout === 'string') {
|
|
29
|
-
cloudflare.timeout = parseInt(cloudflare.timeout);
|
|
25
|
+
cloudflare.timeout = Number.parseInt(cloudflare.timeout);
|
|
30
26
|
}
|
|
31
27
|
if (typeof cloudflare.retry === 'string') {
|
|
32
|
-
cloudflare.retry = parseInt(cloudflare.retry);
|
|
28
|
+
cloudflare.retry = Number.parseInt(cloudflare.retry);
|
|
33
29
|
}
|
|
34
30
|
|
|
35
31
|
// collect all APIs
|
|
@@ -49,10 +45,10 @@ function prepareConfig(config) {
|
|
|
49
45
|
}
|
|
50
46
|
|
|
51
47
|
const { domain, zone } = getDomainAndZone(api);
|
|
52
|
-
const timeout = parseInt(api.timeout || config.timeout, 10) || 250;
|
|
53
|
-
const retry = parseInt(api.retry || config.retry, 10) || 2;
|
|
48
|
+
const timeout = Number.parseInt(api.timeout || config.timeout, 10) || 250;
|
|
49
|
+
const retry = Number.parseInt(api.retry || config.retry, 10) || 2;
|
|
54
50
|
const force = 'force' in api ? api.force : config.force;
|
|
55
|
-
const repair = parseInt(api.repair || config.repair, 10) || 5;
|
|
51
|
+
const repair = Number.parseInt(api.repair || config.repair, 10) || 5;
|
|
56
52
|
const ipv6 = Boolean(api.ipv6 ?? config.ipv6);
|
|
57
53
|
const ipv4 = Boolean(api.ipv4 ?? config.ipv4 ?? true);
|
|
58
54
|
|
|
@@ -88,6 +84,5 @@ function prepareConfig(config) {
|
|
|
88
84
|
force,
|
|
89
85
|
repair
|
|
90
86
|
};
|
|
91
|
-
|
|
92
87
|
}
|
|
93
88
|
}
|
package/lib/dns.js
CHANGED
|
@@ -39,7 +39,10 @@ async function doResolve(domain) {
|
|
|
39
39
|
async function resolveWithFamily(rrtype) {
|
|
40
40
|
try {
|
|
41
41
|
const addresses = await resolver.resolve(domain, rrtype);
|
|
42
|
-
return addresses.map(address => ({
|
|
42
|
+
return addresses.map(address => ({
|
|
43
|
+
address,
|
|
44
|
+
family: RRTYPE_TO_FAMILY[rrtype]
|
|
45
|
+
}));
|
|
43
46
|
} catch (error) {
|
|
44
47
|
debug('Failed to resolve %s with type %s: %s', domain, rrtype, error);
|
|
45
48
|
return [];
|
package/lib/report.js
CHANGED
|
@@ -13,10 +13,21 @@ function collect(context, { url, domain, record, good, switched }) {
|
|
|
13
13
|
if (!good) {
|
|
14
14
|
context.missing.push(url);
|
|
15
15
|
} else if (switched) {
|
|
16
|
-
const line = sprintf(
|
|
16
|
+
const line = sprintf(
|
|
17
|
+
'%-25s %-25s => %-25s [%s]',
|
|
18
|
+
domain,
|
|
19
|
+
record.content,
|
|
20
|
+
good.server,
|
|
21
|
+
good.address
|
|
22
|
+
);
|
|
17
23
|
context.switched.push(line);
|
|
18
24
|
} else {
|
|
19
|
-
const line = sprintf(
|
|
25
|
+
const line = sprintf(
|
|
26
|
+
'%-25s %-25s [%s]',
|
|
27
|
+
record.name,
|
|
28
|
+
good.server,
|
|
29
|
+
good.address
|
|
30
|
+
);
|
|
20
31
|
context.noops.push(line);
|
|
21
32
|
}
|
|
22
33
|
return context;
|
|
@@ -28,11 +39,7 @@ function formatError(err) {
|
|
|
28
39
|
}
|
|
29
40
|
|
|
30
41
|
function reportAll(collected, errors) {
|
|
31
|
-
const {
|
|
32
|
-
missing,
|
|
33
|
-
noops,
|
|
34
|
-
switched
|
|
35
|
-
} = collected;
|
|
42
|
+
const { missing, noops, switched } = collected;
|
|
36
43
|
|
|
37
44
|
let exitCode = 0;
|
|
38
45
|
const lines = [];
|
|
@@ -49,7 +56,7 @@ function reportAll(collected, errors) {
|
|
|
49
56
|
lines.push('No changes:\n', ...noops, '\n');
|
|
50
57
|
}
|
|
51
58
|
if (errors.length) {
|
|
52
|
-
lines.push('Errors:\n', ...errors,
|
|
59
|
+
lines.push('Errors:\n', ...errors, '\n');
|
|
53
60
|
exitCode = -1;
|
|
54
61
|
}
|
|
55
62
|
|
package/lib/state.js
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
import { readFile, writeFile
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
|
|
3
|
-
export {
|
|
4
|
-
updateHealth,
|
|
5
|
-
onExit
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
const {
|
|
9
|
-
ZGRZYT_STATE = '/var/lib/zgrzyt/state.json'
|
|
10
|
-
} = process.env;
|
|
3
|
+
export { updateHealth, onExit };
|
|
11
4
|
|
|
5
|
+
const { ZGRZYT_STATE = '/var/lib/zgrzyt/state.json' } = process.env;
|
|
12
6
|
|
|
13
7
|
const state_p = read();
|
|
14
8
|
let hook;
|
package/lib/zgrzyt.js
CHANGED
|
@@ -3,7 +3,10 @@ import { checkServices } from './check.js';
|
|
|
3
3
|
export async function zgrzyt({ servers, api, client, force, repair }) {
|
|
4
4
|
const { domain, zone } = api;
|
|
5
5
|
const record = await client.listRecords(zone, domain);
|
|
6
|
-
const good = await checkServices(servers, record.content, api, {
|
|
6
|
+
const good = await checkServices(servers, record.content, api, {
|
|
7
|
+
force,
|
|
8
|
+
repair
|
|
9
|
+
});
|
|
7
10
|
const result = {
|
|
8
11
|
url: api.url,
|
|
9
12
|
domain,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zgrzyt",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.3",
|
|
4
4
|
"description": "Poor man's load balancing DNS switcher.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"sprintfjs": "^1.2.16"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@
|
|
35
|
+
"@biomejs/biome": "^1.9.4"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"test": "make check"
|