jetcheck 1.1.5 → 1.1.6
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/config-example.js +2 -0
- package/package.json +1 -2
- package/services/builtin/cmd.js +0 -2
- package/services/builtin/http.js +105 -13
- package/services/builtin/ovh.js +0 -1
- package/services/builtin/ping.js +10 -7
- package/services/client.js +101 -40
package/config-example.js
CHANGED
|
@@ -40,8 +40,10 @@ module.exports = {
|
|
|
40
40
|
// checker tasks
|
|
41
41
|
tasks: [
|
|
42
42
|
// { type: "ping", host: "8.8.8.8", count: 2 },
|
|
43
|
+
// { type: "ping", host: "8.8.8.8", interface: "eth0" },
|
|
43
44
|
{ type: "ping", host: "54.43.5.5" },
|
|
44
45
|
// { type: "http", url: "https://8.8.8.8" },
|
|
46
|
+
// { type: "http", url: "https://1.1.1.1", interface: "eth0" },
|
|
45
47
|
// { type: "http", url: "https://1.1.1.1" },
|
|
46
48
|
// { type: "http", url: "https://1.2.4.1" },
|
|
47
49
|
],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jetcheck",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"description": "Javascript based Network & System checker",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
"author": "Michael VERGOZ",
|
|
14
14
|
"license": "GPL-3.0",
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"axios": "^1.7.9",
|
|
17
16
|
"body-parser": "^1.20.3",
|
|
18
17
|
"ejs": "^3.1.10",
|
|
19
18
|
"elapsed": "^0.0.7",
|
package/services/builtin/cmd.js
CHANGED
package/services/builtin/http.js
CHANGED
|
@@ -1,24 +1,116 @@
|
|
|
1
|
-
const
|
|
1
|
+
const http = require("http")
|
|
2
|
+
const https = require("https")
|
|
3
|
+
const net = require("net")
|
|
4
|
+
const os = require("os")
|
|
5
|
+
|
|
6
|
+
const REDIRECT = new Set([301, 302, 303, 307, 308])
|
|
7
|
+
|
|
8
|
+
function familyOf(entry) {
|
|
9
|
+
if (entry.family === 6 || entry.family === "IPv6")
|
|
10
|
+
return 6
|
|
11
|
+
if (entry.family === 4 || entry.family === "IPv4")
|
|
12
|
+
return 4
|
|
13
|
+
return 0
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isLinkLocal(address, family) {
|
|
17
|
+
if (family === 6)
|
|
18
|
+
return address.toLowerCase().startsWith("fe80:")
|
|
19
|
+
if (family === 4)
|
|
20
|
+
return address.startsWith("169.254.")
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function sourceAddress(iface, prefer) {
|
|
25
|
+
const list = os.networkInterfaces()[iface]
|
|
26
|
+
if (!list || list.length === 0)
|
|
27
|
+
throw new Error(`Network interface not found "${iface}"`)
|
|
28
|
+
|
|
29
|
+
const families = prefer ? [prefer] : [4, 6]
|
|
30
|
+
for (const family of families) {
|
|
31
|
+
const matches = list.filter((entry) => familyOf(entry) === family)
|
|
32
|
+
if (matches.length === 0)
|
|
33
|
+
continue
|
|
34
|
+
const usable = matches.filter((entry) => !isLinkLocal(entry.address, family))
|
|
35
|
+
const pool = usable.length ? usable : matches
|
|
36
|
+
const picked = pool.find((entry) => !entry.internal) || pool[0]
|
|
37
|
+
return { address: picked.address, family }
|
|
38
|
+
}
|
|
39
|
+
throw new Error(`No usable address on interface "${iface}"`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function request(url, timeout, source) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
let settled = false
|
|
45
|
+
const fail = (err) => {
|
|
46
|
+
if (settled)
|
|
47
|
+
return
|
|
48
|
+
settled = true
|
|
49
|
+
reject(err)
|
|
50
|
+
}
|
|
51
|
+
const ok = (value) => {
|
|
52
|
+
if (settled)
|
|
53
|
+
return
|
|
54
|
+
settled = true
|
|
55
|
+
resolve(value)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const target = new URL(url)
|
|
59
|
+
const lib = target.protocol === "https:" ? https : http
|
|
60
|
+
const options = {
|
|
61
|
+
protocol: target.protocol,
|
|
62
|
+
hostname: target.hostname,
|
|
63
|
+
port: target.port || undefined,
|
|
64
|
+
path: `${target.pathname}${target.search}`,
|
|
65
|
+
method: "GET",
|
|
66
|
+
}
|
|
67
|
+
if (source) {
|
|
68
|
+
options.localAddress = source.address
|
|
69
|
+
options.family = source.family
|
|
70
|
+
}
|
|
71
|
+
const req = lib.request(options, (res) => {
|
|
72
|
+
res.on("error", fail)
|
|
73
|
+
res.on("data", () => { })
|
|
74
|
+
res.on("end", () => ok(res))
|
|
75
|
+
})
|
|
76
|
+
req.setTimeout(timeout, () => {
|
|
77
|
+
req.destroy(new Error(`timeout of ${timeout}ms exceeded`))
|
|
78
|
+
})
|
|
79
|
+
req.on("error", fail)
|
|
80
|
+
req.end()
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function check(url, timeout, source, redirectsLeft = 5) {
|
|
85
|
+
const res = await request(url, timeout, source)
|
|
86
|
+
if (REDIRECT.has(res.statusCode) && res.headers.location) {
|
|
87
|
+
if (redirectsLeft <= 0)
|
|
88
|
+
throw new Error("Maximum number of redirects exceeded")
|
|
89
|
+
const next = new URL(res.headers.location, url).toString()
|
|
90
|
+
return check(next, timeout, source, redirectsLeft - 1)
|
|
91
|
+
}
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
2
94
|
|
|
3
95
|
module.exports = async function (kernel) {
|
|
4
96
|
kernel.tests.plugins.http = {
|
|
5
97
|
toString: function (options) {
|
|
6
|
-
|
|
98
|
+
const via = options.interface ? ` via ${options.interface}` : ""
|
|
99
|
+
return (`http(${options.url}${via})`)
|
|
7
100
|
},
|
|
8
101
|
run: async function (options, task, state) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
} catch (error) {
|
|
17
|
-
return error.message;
|
|
102
|
+
const timeout = options.timeout ? options.timeout : 5000
|
|
103
|
+
try {
|
|
104
|
+
let source = null
|
|
105
|
+
if (options.interface) {
|
|
106
|
+
const hostname = new URL(options.url).hostname
|
|
107
|
+
const prefer = net.isIP(hostname)
|
|
108
|
+
source = sourceAddress(options.interface, prefer)
|
|
18
109
|
}
|
|
110
|
+
return await check(options.url, timeout, source)
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return error.message
|
|
19
113
|
}
|
|
20
|
-
return (await check(options.url))
|
|
21
|
-
|
|
22
114
|
}
|
|
23
115
|
}
|
|
24
116
|
console.log("Loading HTTP plugin")
|
package/services/builtin/ovh.js
CHANGED
package/services/builtin/ping.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
|
-
const axios = require('axios');
|
|
2
|
-
|
|
3
1
|
module.exports = async function (kernel) {
|
|
4
2
|
kernel.tests.plugins.ping = {
|
|
5
3
|
toString: function (options) {
|
|
6
|
-
|
|
4
|
+
const via = options.interface ? ` via ${options.interface}` : ""
|
|
5
|
+
return (`ping(${options.host}${via})`)
|
|
7
6
|
},
|
|
8
7
|
run: async function (options, task, state) {
|
|
9
8
|
const count = options.count ? options.count : 3
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
// macOS binds the socket with -b. Linux accepts the interface name with -I.
|
|
10
|
+
const bind = options.interface
|
|
11
|
+
? (process.platform === "darwin" ? `-b ${options.interface} ` : `-I ${options.interface} `)
|
|
12
|
+
: ""
|
|
13
|
+
const ret = await kernel.lib.exec(`ping -c ${count} ${bind}${options.host}`)
|
|
14
|
+
if (ret.error > 0)
|
|
15
|
+
return (ret.stderr || "Host down")
|
|
16
|
+
return (null)
|
|
14
17
|
}
|
|
15
18
|
}
|
|
16
19
|
console.log("Loading PING plugin")
|
package/services/client.js
CHANGED
|
@@ -1,57 +1,118 @@
|
|
|
1
|
-
const
|
|
1
|
+
const http = require('http')
|
|
2
2
|
const https = require('https')
|
|
3
|
-
const
|
|
4
|
-
const os = require('os')
|
|
3
|
+
const os = require('os')
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
this.account = account;
|
|
10
|
-
this.endpoint = "/api/v1"
|
|
5
|
+
function joinURL(base, path) {
|
|
6
|
+
return base.replace(/\/+$/, '') + '/' + path.replace(/^\/+/, '')
|
|
7
|
+
}
|
|
11
8
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
9
|
+
function request(method, url, { headers, body, timeout, agent }) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
let settled = false
|
|
12
|
+
const fail = (err) => {
|
|
13
|
+
if (settled)
|
|
14
|
+
return
|
|
15
|
+
settled = true
|
|
16
|
+
reject(err)
|
|
17
|
+
}
|
|
18
|
+
const ok = (value) => {
|
|
19
|
+
if (settled)
|
|
20
|
+
return
|
|
21
|
+
settled = true
|
|
22
|
+
resolve(value)
|
|
23
|
+
}
|
|
27
24
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
const target = new URL(url)
|
|
26
|
+
const lib = target.protocol === 'https:' ? https : http
|
|
27
|
+
const payload = body == null ? null : Buffer.from(JSON.stringify(body))
|
|
28
|
+
const reqHeaders = { ...headers }
|
|
29
|
+
if (payload) {
|
|
30
|
+
reqHeaders['Content-Type'] = 'application/json'
|
|
31
|
+
reqHeaders['Content-Length'] = String(payload.length)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const req = lib.request({
|
|
35
|
+
protocol: target.protocol,
|
|
36
|
+
hostname: target.hostname,
|
|
37
|
+
port: target.port || undefined,
|
|
38
|
+
path: `${target.pathname}${target.search}`,
|
|
39
|
+
method,
|
|
40
|
+
headers: reqHeaders,
|
|
41
|
+
agent: target.protocol === 'https:' ? agent : undefined,
|
|
42
|
+
}, (res) => {
|
|
43
|
+
const chunks = []
|
|
44
|
+
res.on('data', (chunk) => chunks.push(chunk))
|
|
45
|
+
res.on('error', fail)
|
|
46
|
+
res.on('end', () => {
|
|
47
|
+
const raw = Buffer.concat(chunks).toString('utf8')
|
|
48
|
+
const type = String(res.headers['content-type'] || '')
|
|
49
|
+
if (!type.includes('application/json')) {
|
|
50
|
+
ok({ status: res.statusCode, data: raw })
|
|
31
51
|
return
|
|
32
52
|
}
|
|
33
|
-
|
|
34
|
-
|
|
53
|
+
try {
|
|
54
|
+
ok({ status: res.statusCode, data: raw.length ? JSON.parse(raw) : null })
|
|
55
|
+
} catch (err) {
|
|
56
|
+
fail(err)
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
req.setTimeout(timeout, () => {
|
|
62
|
+
req.destroy(new Error(`timeout of ${timeout}ms exceeded`))
|
|
63
|
+
})
|
|
64
|
+
req.on('error', fail)
|
|
65
|
+
if (payload)
|
|
66
|
+
req.write(payload)
|
|
67
|
+
req.end()
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class JetcheckAPI {
|
|
72
|
+
constructor(kernel, account) {
|
|
73
|
+
this.kernel = kernel
|
|
74
|
+
this.account = account
|
|
75
|
+
this.endpoint = "/api/v1"
|
|
76
|
+
this.timeout = account.timeout || 2000
|
|
77
|
+
this.headers = {
|
|
78
|
+
'Authorization': `${account.token}`,
|
|
79
|
+
'User-Agent': `Jetcheck client v${this.kernel.version}`,
|
|
80
|
+
'Accept': 'application/json, text/plain, */*',
|
|
81
|
+
}
|
|
82
|
+
this.agent = new https.Agent({
|
|
83
|
+
rejectUnauthorized: false,
|
|
84
|
+
}).on('keylog', (line, tlsSocket) => {
|
|
85
|
+
const cert = tlsSocket.getPeerCertificate(false)
|
|
86
|
+
if (!cert || !cert.fingerprint256)
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
if (this.account.fingerprint !== cert.fingerprint256) {
|
|
90
|
+
console.log(`Bad fingerprint on certificate ${this.account.url} local=${this.account.fingerprint} remote=${cert.fingerprint256}`)
|
|
91
|
+
tlsSocket.destroy()
|
|
92
|
+
}
|
|
93
|
+
})
|
|
35
94
|
}
|
|
36
95
|
|
|
37
|
-
async
|
|
38
|
-
// console.log(`GET ${this.account.url}${this.endpoint}${rcs}`);
|
|
96
|
+
async call(method, rcs, data) {
|
|
39
97
|
try {
|
|
40
|
-
const response = await this.
|
|
41
|
-
|
|
98
|
+
const response = await request(method, joinURL(this.account.url, `${this.endpoint}${rcs}`), {
|
|
99
|
+
headers: this.headers,
|
|
100
|
+
body: data,
|
|
101
|
+
timeout: this.timeout,
|
|
102
|
+
agent: this.agent,
|
|
103
|
+
})
|
|
104
|
+
return { err: null, data: response.data }
|
|
42
105
|
} catch (err) {
|
|
43
|
-
return { err: err.message, data: null }
|
|
106
|
+
return { err: err.message, data: null }
|
|
44
107
|
}
|
|
45
108
|
}
|
|
46
109
|
|
|
110
|
+
async get(rcs) {
|
|
111
|
+
return this.call('GET', rcs)
|
|
112
|
+
}
|
|
113
|
+
|
|
47
114
|
async post(rcs, data) {
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
const response = await this.handler.post(`${this.endpoint}${rcs}`, data);
|
|
51
|
-
return { err: null, data: response.data };
|
|
52
|
-
} catch (err) {
|
|
53
|
-
return { err: err.message, data: null };
|
|
54
|
-
}
|
|
115
|
+
return this.call('POST', rcs, data)
|
|
55
116
|
}
|
|
56
117
|
}
|
|
57
118
|
|