pinokiod 8.2.10 → 8.2.11
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/kernel/anywhere/cli.js +100 -0
- package/kernel/anywhere/index.js +236 -160
- package/kernel/anywhere/ports.js +6 -9
- package/kernel/anywhere/serve.js +17 -7
- package/kernel/anywhere/state.js +14 -0
- package/kernel/bin/conda-meta.js +19 -1
- package/kernel/bin/conda-pins.js +7 -3
- package/kernel/bin/conda.js +67 -15
- package/kernel/bin/index.js +5 -2
- package/kernel/bin/setup.js +3 -17
- package/kernel/bin/tailscaled.js +23 -13
- package/kernel/index.js +11 -9
- package/kernel/shell.js +14 -6
- package/kernel/util.js +1 -0
- package/package.json +1 -1
- package/server/index.js +15 -19
- package/server/public/universal-launcher.js +5 -0
- package/server/views/anywhere.ejs +13 -13
- package/server/views/partials/main_sidebar.ejs +0 -1
- package/server/views/remote.ejs +12 -4
- package/system/pip_seed.json +6 -0
- package/system/repair_pip_seed.py +100 -0
- package/test/anywhere-remote.test.js +182 -8
- package/test/anywhere.test.js +1051 -131
- package/test/conda-bin.test.js +254 -4
- package/test/pip_seed_test.py +183 -0
- package/test/shell-run-template.test.js +147 -140
- package/test/tailscale-cli.test.js +239 -0
- package/kernel/anywhere/localapi.js +0 -147
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Minimal client for the tailscaled LocalAPI.
|
|
3
|
-
|
|
4
|
-
tailscaled exposes an HTTP API over a unix socket (mac/linux) or a named
|
|
5
|
-
pipe (Windows). We talk to it with plain node `http` by passing the socket
|
|
6
|
-
path as `socketPath`; node's net layer handles both unix sockets and
|
|
7
|
-
`\\.\pipe\...` names. The conda-forge `tailscaled` package ships only the
|
|
8
|
-
daemon (no `tailscale` CLI), so every operation goes through this client.
|
|
9
|
-
|
|
10
|
-
Verified against tailscaled 1.104.1 (see spec/requirements/pinokio-anywhere.md):
|
|
11
|
-
GET /localapi/v0/status -> ipnstate.Status JSON
|
|
12
|
-
GET /localapi/v0/prefs -> ipn.Prefs JSON
|
|
13
|
-
PATCH /localapi/v0/prefs <- ipn.MaskedPrefs ({HostnameSet:true, Hostname:"pinokio", ...})
|
|
14
|
-
POST /localapi/v0/login-interactive -> 204; AuthURL shows up in /status and on the IPN bus (BrowseToURL)
|
|
15
|
-
POST /localapi/v0/logout -> 204
|
|
16
|
-
GET /localapi/v0/serve-config -> ipn.ServeConfig JSON (or null) + Etag header
|
|
17
|
-
POST /localapi/v0/serve-config <- ipn.ServeConfig JSON, optional If-Match: <etag>
|
|
18
|
-
POST /localapi/v0/query-feature?feature=serve -> {Complete, Text, URL} (URL = one-click tailnet approval)
|
|
19
|
-
*/
|
|
20
|
-
const http = require('http')
|
|
21
|
-
|
|
22
|
-
class LocalAPIError extends Error {
|
|
23
|
-
constructor(message, status, body) {
|
|
24
|
-
super(message)
|
|
25
|
-
this.name = 'LocalAPIError'
|
|
26
|
-
this.status = status
|
|
27
|
-
this.body = body
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
class LocalAPI {
|
|
32
|
-
constructor({ socketPath, timeout = 10000 } = {}) {
|
|
33
|
-
if (!socketPath) {
|
|
34
|
-
throw new Error('LocalAPI requires a socketPath')
|
|
35
|
-
}
|
|
36
|
-
this.socketPath = socketPath
|
|
37
|
-
this.timeout = timeout
|
|
38
|
-
this.base = '/localapi/v0'
|
|
39
|
-
}
|
|
40
|
-
request(method, route, { body, headers, timeout } = {}) {
|
|
41
|
-
const payload = typeof body === 'undefined' || body === null
|
|
42
|
-
? null
|
|
43
|
-
: (typeof body === 'string' || Buffer.isBuffer(body) ? body : JSON.stringify(body))
|
|
44
|
-
const reqHeaders = Object.assign({
|
|
45
|
-
// tailscaled rejects non-loopback origins without this on some
|
|
46
|
-
// transports (the Windows GUI loopback port); harmless on sockets.
|
|
47
|
-
'Sec-Tailscale': 'localapi',
|
|
48
|
-
'Host': 'local-tailscaled.sock',
|
|
49
|
-
}, headers || {})
|
|
50
|
-
if (payload !== null) {
|
|
51
|
-
reqHeaders['Content-Type'] = reqHeaders['Content-Type'] || 'application/json'
|
|
52
|
-
reqHeaders['Content-Length'] = Buffer.byteLength(payload)
|
|
53
|
-
}
|
|
54
|
-
return new Promise((resolve, reject) => {
|
|
55
|
-
const req = http.request({
|
|
56
|
-
socketPath: this.socketPath,
|
|
57
|
-
method,
|
|
58
|
-
path: this.base + route,
|
|
59
|
-
headers: reqHeaders,
|
|
60
|
-
}, (res) => {
|
|
61
|
-
const chunks = []
|
|
62
|
-
res.on('data', (chunk) => chunks.push(chunk))
|
|
63
|
-
res.on('end', () => {
|
|
64
|
-
const text = Buffer.concat(chunks).toString('utf8')
|
|
65
|
-
resolve({
|
|
66
|
-
status: res.statusCode,
|
|
67
|
-
headers: res.headers,
|
|
68
|
-
text,
|
|
69
|
-
json: () => {
|
|
70
|
-
if (!text || !text.trim()) return null
|
|
71
|
-
return JSON.parse(text)
|
|
72
|
-
},
|
|
73
|
-
})
|
|
74
|
-
})
|
|
75
|
-
res.on('error', reject)
|
|
76
|
-
})
|
|
77
|
-
req.setTimeout(timeout || this.timeout, () => {
|
|
78
|
-
req.destroy(new Error(`LocalAPI ${method} ${route} timed out`))
|
|
79
|
-
})
|
|
80
|
-
req.on('error', reject)
|
|
81
|
-
if (payload !== null) {
|
|
82
|
-
req.write(payload)
|
|
83
|
-
}
|
|
84
|
-
req.end()
|
|
85
|
-
})
|
|
86
|
-
}
|
|
87
|
-
async expect(method, route, options, okStatuses = [200, 204]) {
|
|
88
|
-
const res = await this.request(method, route, options)
|
|
89
|
-
if (!okStatuses.includes(res.status)) {
|
|
90
|
-
throw new LocalAPIError(`LocalAPI ${method} ${route} failed: ${res.status} ${res.text.trim()}`, res.status, res.text)
|
|
91
|
-
}
|
|
92
|
-
return res
|
|
93
|
-
}
|
|
94
|
-
async status() {
|
|
95
|
-
const res = await this.expect('GET', '/status')
|
|
96
|
-
return res.json()
|
|
97
|
-
}
|
|
98
|
-
// masked: an ipn.MaskedPrefs object, e.g. { HostnameSet: true, Hostname: 'pinokio' }
|
|
99
|
-
async patchPrefs(masked) {
|
|
100
|
-
const res = await this.expect('PATCH', '/prefs', { body: masked })
|
|
101
|
-
return res.json()
|
|
102
|
-
}
|
|
103
|
-
async loginInteractive() {
|
|
104
|
-
await this.expect('POST', '/login-interactive', { body: '' })
|
|
105
|
-
return true
|
|
106
|
-
}
|
|
107
|
-
async logout() {
|
|
108
|
-
await this.expect('POST', '/logout', { body: '' })
|
|
109
|
-
return true
|
|
110
|
-
}
|
|
111
|
-
// Restarts the backend's login state machine (what `tailscale up --force-reauth`
|
|
112
|
-
// does): a stale or invalidated login URL is dropped and a fresh one is minted
|
|
113
|
-
// on the next login-interactive.
|
|
114
|
-
async start(options = {}) {
|
|
115
|
-
await this.expect('POST', '/start', { body: options })
|
|
116
|
-
return true
|
|
117
|
-
}
|
|
118
|
-
async serveConfig() {
|
|
119
|
-
const res = await this.expect('GET', '/serve-config')
|
|
120
|
-
return { config: res.json(), etag: res.headers.etag || res.headers.Etag || null }
|
|
121
|
-
}
|
|
122
|
-
async setServeConfig(config, etag) {
|
|
123
|
-
const headers = {}
|
|
124
|
-
if (etag) {
|
|
125
|
-
headers['If-Match'] = etag
|
|
126
|
-
}
|
|
127
|
-
await this.expect('POST', '/serve-config', { body: config || {}, headers })
|
|
128
|
-
return true
|
|
129
|
-
}
|
|
130
|
-
async queryFeature(feature) {
|
|
131
|
-
const res = await this.request('POST', `/query-feature?feature=${encodeURIComponent(feature)}`, { body: '' })
|
|
132
|
-
if (res.status !== 200) {
|
|
133
|
-
throw new LocalAPIError(`query-feature ${feature} failed: ${res.status} ${res.text.trim()}`, res.status, res.text)
|
|
134
|
-
}
|
|
135
|
-
return res.json()
|
|
136
|
-
}
|
|
137
|
-
async ping() {
|
|
138
|
-
try {
|
|
139
|
-
const res = await this.request('GET', '/status', { timeout: 2000 })
|
|
140
|
-
return res.status === 200
|
|
141
|
-
} catch (e) {
|
|
142
|
-
return false
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
module.exports = { LocalAPI, LocalAPIError }
|