pinokiod 8.2.10 → 8.2.12

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.
@@ -0,0 +1,100 @@
1
+ const { execFile } = require('child_process')
2
+ const net = require('net')
3
+ const { isDeepStrictEqual } = require('util')
4
+ const { FIRST_PORT, LAST_PORT } = require('./ports')
5
+
6
+ // All control goes through the pinned, bundled CLI and this explicit endpoint.
7
+ class TailscaleCLI {
8
+ constructor({ binPath, socketPath }) {
9
+ this.binPath = binPath
10
+ this.socketPath = socketPath
11
+ this.children = new Set()
12
+ }
13
+ run(args, { timeout = 10000, onOutput } = {}) {
14
+ return new Promise((resolve, reject) => {
15
+ const child = execFile(this.binPath, ['--socket=' + this.socketPath, ...args], {
16
+ shell: false, windowsHide: true, timeout, maxBuffer: 4 * 1024 * 1024,
17
+ }, (error, stdout, stderr) => {
18
+ this.children.delete(child)
19
+ if (error) {
20
+ error.message = `tailscale ${args[0]}: ${stderr.trim() || stdout.trim() || error.message}`
21
+ error.cancelled = !!child.cancelled
22
+ reject(error)
23
+ } else resolve(stdout)
24
+ })
25
+ this.children.add(child)
26
+ if (onOutput) {
27
+ let output = ''
28
+ child.stdout.on('data', (chunk) => {
29
+ output += chunk
30
+ try { if (onOutput) onOutput(output) }
31
+ catch (error) { onOutput = null; child.kill(); reject(error) }
32
+ })
33
+ }
34
+ })
35
+ }
36
+ cancel() {
37
+ for (const child of this.children) { child.cancelled = true; child.kill() }
38
+ }
39
+ async json(args, options) {
40
+ return JSON.parse(await this.run(args, options))
41
+ }
42
+ status(timeout = 10000) { return this.json(['status', '--json'], { timeout }) }
43
+ async ping() {
44
+ try {
45
+ await this.status(2000)
46
+ return true
47
+ } catch (error) {
48
+ // A CLI failure alone says nothing about liveness. Only a missing/refused
49
+ // endpoint confirms absence; this connection probe sends no API requests.
50
+ return new Promise((resolve, reject) => {
51
+ const socket = net.createConnection(this.socketPath)
52
+ socket.once('connect', () => { socket.destroy(); reject(error) })
53
+ socket.once('error', (cause) => {
54
+ if (cause.code === 'ENOENT' || cause.code === 'ECONNREFUSED') resolve(false)
55
+ else reject(error)
56
+ })
57
+ socket.setTimeout(2000, () => { socket.destroy(); reject(error) })
58
+ })
59
+ }
60
+ }
61
+ setPrefs(hostname) {
62
+ return this.run(['set', '--hostname=' + hostname, '--accept-dns=false', '--accept-routes=false', '--exit-node='])
63
+ }
64
+ up(onUpdate, force = false, hostname = 'pinokio') {
65
+ let emitted = 0
66
+ return this.run(['up', '--json', '--timeout=5m', '--hostname=' + hostname,
67
+ '--accept-dns=false', '--accept-routes=false', '--exit-node=', ...(force ? ['--force-reauth'] : [])], {
68
+ timeout: 305000,
69
+ onOutput(output) {
70
+ // The pinned CLI emits pretty-printed top-level JSON objects.
71
+ for (const block of (output.match(/^\{[\s\S]*?^\}/gm) || []).slice(emitted)) {
72
+ emitted += 1
73
+ onUpdate(JSON.parse(block))
74
+ }
75
+ },
76
+ })
77
+ }
78
+ logout() { return this.run(['logout']) }
79
+ queryFeature() { return this.json(['debug', 'localapi', 'POST', '/localapi/v0/query-feature?feature=serve']) }
80
+ serveConfig() { return this.json(['serve', 'status', '--json']) }
81
+ async setServeConfig(desired, current) {
82
+ current = current || {}
83
+ for (const [hostPort, web] of Object.entries(desired.Web)) {
84
+ const port = hostPort.slice(hostPort.lastIndexOf(':') + 1)
85
+ if (isDeepStrictEqual(current.Web && current.Web[hostPort], web) &&
86
+ isDeepStrictEqual(current.TCP && current.TCP[port], desired.TCP[port])) continue
87
+ // Each command reads the latest config and writes with its ETag. The CLI
88
+ // reports conflicts; never use set-raw/reset, which lose that protection.
89
+ await this.run(['serve', '--bg', '--https=' + port, web.Handlers['/'].Proxy])
90
+ }
91
+ for (const port of Object.keys(current.TCP || {})) {
92
+ if (!desired.TCP[port] && Number(port) >= FIRST_PORT && Number(port) <= LAST_PORT) {
93
+ await this.run(['serve', '--https=' + port, 'off'])
94
+ }
95
+ }
96
+ return this.serveConfig()
97
+ }
98
+ }
99
+
100
+ module.exports = TailscaleCLI