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 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.5",
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",
@@ -1,5 +1,3 @@
1
- const axios = require('axios')
2
-
3
1
  module.exports = async function (kernel) {
4
2
  kernel.tests.plugins.cmd = {
5
3
  toString: function (options) {
@@ -1,24 +1,116 @@
1
- const axios = require('axios');
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
- return (`http(${options.url})`)
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
- async function check(url) {
10
- try {
11
- await axios.get(url, {
12
- timeout: options.timeout ? options.timeout : 5000,
13
- validateStatus: () => true,
14
- });
15
- return null;
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")
@@ -1,4 +1,3 @@
1
- const axios = require('axios')
2
1
  const ovh = require('ovh')
3
2
 
4
3
 
@@ -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
- return (`ping(${options.host})`)
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
- const ret = await kernel.lib.exec(`ping -c ${count} ${options.host}`)
11
- if(ret.error > 0)
12
- return("Host down")
13
- return(null)
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")
@@ -1,57 +1,118 @@
1
- const axios = require('axios');
1
+ const http = require('http')
2
2
  const https = require('https')
3
- const crypto = require('crypto')
4
- const os = require('os')
3
+ const os = require('os')
5
4
 
6
- class JetcheckAPI {
7
- constructor(kernel, account) {
8
- this.kernel = kernel
9
- this.account = account;
10
- this.endpoint = "/api/v1"
5
+ function joinURL(base, path) {
6
+ return base.replace(/\/+$/, '') + '/' + path.replace(/^\/+/, '')
7
+ }
11
8
 
12
- // Initializing Axios instance with base configuration
13
- this.handler = axios.create({
14
- baseURL: account.url, // Replace with the base URL of the API
15
- headers: {
16
- 'Authorization': `${account.token}`, // Example of token-based authentication
17
- 'User-Agent': `Jetcheck client v${this.kernel.version}`, // Add appKey in headers
18
- },
19
- timeout: account.timeout || 2000, // Default timeout of 5 seconds
20
- validateStatus: () => true,
21
- httpsAgent: new https.Agent({
22
- rejectUnauthorized: false,
23
- }).on('keylog', (line, tlsSocket) => {
24
- const cert = tlsSocket.getPeerCertificate(false)
25
- if (!cert || !cert.fingerprint256)
26
- return
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
- if (this.account.fingerprint !== cert.fingerprint256) {
29
- console.log(`Bad fingerprint on certificate ${this.account.url} local=${this.account.fingerprint} remote=${cert.fingerprint256}`)
30
- tlsSocket.destroy()
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 get(rcs) {
38
- // console.log(`GET ${this.account.url}${this.endpoint}${rcs}`);
96
+ async call(method, rcs, data) {
39
97
  try {
40
- const response = await this.handler.get(`${this.endpoint}${rcs}`);
41
- return { err: null, data: response.data };
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
- // console.log(`POST ${this.account.url}${this.endpoint}${rcs}`);
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