@avelor/mesh 0.2.0 → 0.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/bin/mesh.js +44 -5
- package/package.json +1 -1
- package/src/hosts.js +1 -1
- package/src/proxy.js +28 -5
package/bin/mesh.js
CHANGED
|
@@ -14,6 +14,7 @@ const STATE_FILE = '/tmp/.mesh.json'
|
|
|
14
14
|
const RESET = '\x1b[0m'
|
|
15
15
|
const DIM = '\x1b[2m'
|
|
16
16
|
const GREEN = '\x1b[32m'
|
|
17
|
+
const YELLOW = '\x1b[33m'
|
|
17
18
|
const CYAN = '\x1b[36m'
|
|
18
19
|
|
|
19
20
|
// ── Argument parsing ──────────────────────────────────────────────────────────
|
|
@@ -57,9 +58,36 @@ if (cmd === 'start') {
|
|
|
57
58
|
detached: true,
|
|
58
59
|
stdio: 'ignore',
|
|
59
60
|
})
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
|
|
62
|
+
const pid = child.pid
|
|
63
|
+
const deadline = Date.now() + 5000
|
|
64
|
+
|
|
65
|
+
;(function poll() {
|
|
66
|
+
let alive = true
|
|
67
|
+
try { process.kill(pid, 0) } catch (e) { if (e.code !== 'EPERM') alive = false }
|
|
68
|
+
|
|
69
|
+
if (!alive) {
|
|
70
|
+
console.error('mesh: failed to start — check your mesh.yml and that ports 80/443 are free')
|
|
71
|
+
process.exit(1)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const state = JSON.parse(readFileSync(STATE_FILE, 'utf8'))
|
|
76
|
+
if (state.pid === pid) {
|
|
77
|
+
child.unref()
|
|
78
|
+
console.log(`mesh: started (pid ${pid})`)
|
|
79
|
+
process.exit(0)
|
|
80
|
+
}
|
|
81
|
+
} catch { /* not ready yet */ }
|
|
82
|
+
|
|
83
|
+
if (Date.now() >= deadline) {
|
|
84
|
+
child.kill()
|
|
85
|
+
console.error('mesh: timed out waiting for proxy to start')
|
|
86
|
+
process.exit(1)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setTimeout(poll, 50)
|
|
90
|
+
})()
|
|
63
91
|
}
|
|
64
92
|
|
|
65
93
|
if (cmd === 'stop') {
|
|
@@ -120,6 +148,17 @@ if (cmd === 'status') {
|
|
|
120
148
|
for (const [name, port] of Object.entries(state.services)) {
|
|
121
149
|
console.log(` ${GREEN}${name.padEnd(pad)}.test${RESET} ${DIM}→ :${port} ${protocol}://${name}.test${RESET}`)
|
|
122
150
|
}
|
|
151
|
+
const rules = state.rules ?? {}
|
|
152
|
+
if (Object.keys(rules).length) {
|
|
153
|
+
console.log('')
|
|
154
|
+
for (const [svc, ruleList] of Object.entries(rules)) {
|
|
155
|
+
for (const r of ruleList) {
|
|
156
|
+
const type = r.status ? `${r.status}` : `${r.delay}ms delay`
|
|
157
|
+
const methodStr = r.method ? `${r.method} ` : ''
|
|
158
|
+
console.log(` ${YELLOW}${svc}${r.path}${RESET} ${DIM}${methodStr}${r.rate}% → ${type}${RESET}`)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
123
162
|
console.log('')
|
|
124
163
|
process.exit(0)
|
|
125
164
|
}
|
|
@@ -160,7 +199,7 @@ const configDir = dirname(resolve(configPath))
|
|
|
160
199
|
const certs = ensureCerts(services, configDir)
|
|
161
200
|
const servers = startProxy(services, rules, certs)
|
|
162
201
|
|
|
163
|
-
writeFileSync(STATE_FILE, JSON.stringify({ pid: process.pid, configPath, services, https: !!certs }))
|
|
202
|
+
writeFileSync(STATE_FILE, JSON.stringify({ pid: process.pid, configPath, services, rules, https: !!certs }))
|
|
164
203
|
|
|
165
204
|
// ── Crash safety — clean /etc/hosts even on unexpected exit ───────────────────
|
|
166
205
|
|
|
@@ -202,7 +241,7 @@ watch(configPath, () => {
|
|
|
202
241
|
Object.assign(rules, next.rules)
|
|
203
242
|
|
|
204
243
|
writeHosts(services)
|
|
205
|
-
writeFileSync(STATE_FILE, JSON.stringify({ pid: process.pid, configPath, services, https: !!certs }))
|
|
244
|
+
writeFileSync(STATE_FILE, JSON.stringify({ pid: process.pid, configPath, services, rules, https: !!certs }))
|
|
206
245
|
|
|
207
246
|
if (certs && hasNewServices) {
|
|
208
247
|
const newCerts = ensureCerts(services, configDir)
|
package/package.json
CHANGED
package/src/hosts.js
CHANGED
|
@@ -8,7 +8,7 @@ export function writeHosts(services, hostsFile = HOSTS_FILE) {
|
|
|
8
8
|
const current = readFileSync(hostsFile, 'utf8')
|
|
9
9
|
const clean = removeMeshBlock(current)
|
|
10
10
|
const entries = Object.keys(services)
|
|
11
|
-
.
|
|
11
|
+
.flatMap(name => [`127.0.0.1 ${name}.test`, `::1 ${name}.test`])
|
|
12
12
|
.join('\n')
|
|
13
13
|
const next = `${clean.trimEnd()}\n\n${MESH_START}\n${entries}\n${MESH_END}\n`
|
|
14
14
|
writeFileSync(hostsFile, next, 'utf8')
|
package/src/proxy.js
CHANGED
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
import http from 'http'
|
|
2
2
|
import https from 'https'
|
|
3
|
+
import net from 'net'
|
|
3
4
|
import { readFileSync } from 'fs'
|
|
4
5
|
import httpProxy from 'http-proxy'
|
|
5
6
|
import { matchRule, applyRule } from './rules.js'
|
|
6
7
|
import { wantsHtml, errorPage } from './error-page.js'
|
|
7
8
|
|
|
9
|
+
const hostCache = new Map()
|
|
10
|
+
|
|
11
|
+
function probeHost(port) {
|
|
12
|
+
if (hostCache.has(port)) return Promise.resolve(hostCache.get(port))
|
|
13
|
+
const probe = addr => new Promise((resolve, reject) => {
|
|
14
|
+
const s = net.connect(port, addr)
|
|
15
|
+
s.setTimeout(200)
|
|
16
|
+
s.on('connect', () => { s.destroy(); resolve(addr) })
|
|
17
|
+
s.on('timeout', () => { s.destroy(); reject(new Error('timeout')) })
|
|
18
|
+
s.on('error', reject)
|
|
19
|
+
})
|
|
20
|
+
return Promise.any([probe('127.0.0.1'), probe('::1')])
|
|
21
|
+
.then(host => { hostCache.set(port, host); return host })
|
|
22
|
+
.catch(() => '127.0.0.1')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function fmtHost(host) {
|
|
26
|
+
return host.includes(':') ? `[${host}]` : host
|
|
27
|
+
}
|
|
28
|
+
|
|
8
29
|
const RESET = '\x1b[0m'
|
|
9
30
|
const DIM = '\x1b[2m'
|
|
10
31
|
const GREEN = '\x1b[32m'
|
|
@@ -65,14 +86,16 @@ export function startProxy(services, rules, certs = null) {
|
|
|
65
86
|
log(YELLOW, `${rule.delay}ms`, name, pathname, `→ :${target} (delayed)`)
|
|
66
87
|
}
|
|
67
88
|
|
|
68
|
-
|
|
89
|
+
const host = await probeHost(target)
|
|
90
|
+
proxy.web(req, res, { target: `http://${fmtHost(host)}:${target}` })
|
|
69
91
|
if (!rule) log(DIM, '→', name, pathname, `→ :${target}`)
|
|
70
92
|
}
|
|
71
93
|
|
|
72
|
-
function handleUpgrade(req, socket, head) {
|
|
94
|
+
async function handleUpgrade(req, socket, head) {
|
|
73
95
|
const { name, target } = resolveService(req.headers.host)
|
|
74
96
|
if (!target) { socket.destroy(); return }
|
|
75
|
-
|
|
97
|
+
const host = await probeHost(target)
|
|
98
|
+
proxy.ws(req, socket, head, { target: `ws://${fmtHost(host)}:${target}` }, err => {
|
|
76
99
|
if (err) log(RED, 'WSE', name, req.url, `→ ${err.code ?? err.message}`)
|
|
77
100
|
})
|
|
78
101
|
log(DIM, 'WS', name, req.url, `→ :${target}`)
|
|
@@ -98,7 +121,7 @@ export function startProxy(services, rules, certs = null) {
|
|
|
98
121
|
throw err
|
|
99
122
|
})
|
|
100
123
|
|
|
101
|
-
httpServer.listen(80, '
|
|
124
|
+
httpServer.listen(80, '::', () => onReady(services, rules, certs))
|
|
102
125
|
|
|
103
126
|
let httpsServer = null
|
|
104
127
|
|
|
@@ -115,7 +138,7 @@ export function startProxy(services, rules, certs = null) {
|
|
|
115
138
|
}
|
|
116
139
|
throw err
|
|
117
140
|
})
|
|
118
|
-
httpsServer.listen(443, '
|
|
141
|
+
httpsServer.listen(443, '::')
|
|
119
142
|
}
|
|
120
143
|
|
|
121
144
|
return { http: httpServer, https: httpsServer }
|