pinokiod 8.2.32 → 8.2.33

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.
@@ -30,6 +30,14 @@ function openPath(label, port, to) {
30
30
  return `/anywhere/peers/${encodeURIComponent(label)}/open?port=${Number(port)}&to=${encodeURIComponent(to || '/')}`
31
31
  }
32
32
 
33
+ // The headers go out as the incoming request's headers. Setting them in http-proxy's proxyReq
34
+ // hook instead would throw: with the proxy agent the hook fires once the CONNECT completes, and
35
+ // by then the outgoing headers are already written.
36
+ function mark(req) {
37
+ for (const name of STRIPPED_HEADERS) delete req.headers[name]
38
+ req.headers[FORWARDED_HEADER] = '1'
39
+ }
40
+
33
41
  class Forwarders {
34
42
  // proxyUrl: () => 'http://127.0.0.1:<port>' or null while the daemon is down
35
43
  // serverPort: () => pinokiod's own port
@@ -83,13 +91,6 @@ class Forwarders {
83
91
  res.destroy()
84
92
  }
85
93
  })
86
- // The headers go out as the incoming request's headers. Setting them in http-proxy's proxyReq
87
- // hook instead would throw: with the proxy agent the hook fires once the CONNECT completes, and
88
- // by then the outgoing headers are already written.
89
- const mark = (req) => {
90
- for (const name of STRIPPED_HEADERS) delete req.headers[name]
91
- req.headers[FORWARDED_HEADER] = '1'
92
- }
93
94
  const server = http.createServer((req, res) => {
94
95
  const url = new URL(req.url, 'http://127.0.0.1')
95
96
  if (url.pathname === OPEN_PATH) {
@@ -142,7 +143,7 @@ class Forwarders {
142
143
  }
143
144
  }
144
145
 
145
- // Resolves { status, body }; rejects on a transport failure or when the whole
146
+ // Resolves { status, headers, raw, body }; rejects on a transport failure or when the whole
146
147
  // request, the proxy's CONNECT included, exceeds the deadline.
147
148
  function requestThroughProxy(agent, url, { method = 'GET', headers = {}, body = null, timeoutMs = 4000 } = {}) {
148
149
  return new Promise((resolve, reject) => {
@@ -154,7 +155,11 @@ function requestThroughProxy(agent, url, { method = 'GET', headers = {}, body =
154
155
  const req = lib.request(target, { method, headers, agent }, (res) => {
155
156
  const chunks = []
156
157
  res.on('data', (chunk) => chunks.push(chunk))
157
- res.on('end', () => { clearTimeout(deadline); resolve({ status: res.statusCode, body: Buffer.concat(chunks).toString('utf8') }) })
158
+ res.on('end', () => {
159
+ clearTimeout(deadline)
160
+ const raw = Buffer.concat(chunks)
161
+ resolve({ status: res.statusCode, headers: res.headers, raw, body: raw.toString('utf8') })
162
+ })
158
163
  res.on('error', (error) => { clearTimeout(deadline); reject(error) })
159
164
  })
160
165
  req.on('error', (error) => { clearTimeout(deadline); reject(error) })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "8.2.32",
3
+ "version": "8.2.33",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -1069,7 +1069,7 @@ class Server {
1069
1069
  anywherePeerAllowed(req) {
1070
1070
  if (!this.isLocalRequest(req)) return false
1071
1071
  const origin = req.get("Origin")
1072
- if (!origin) return true
1072
+ if (!origin || origin === "https://pinokio.co") return true
1073
1073
  try { return new URL(origin).host.toLowerCase() === String(req.get("host")).toLowerCase() } catch (_) { return false }
1074
1074
  }
1075
1075
  // the reason the peer routes cannot work right now, or null
@@ -1096,6 +1096,7 @@ class Server {
1096
1096
  const apps = Array.isArray(status.apps) ? status.apps : []
1097
1097
  return {
1098
1098
  name: label,
1099
+ address: `https://${peer.dnsName}`,
1099
1100
  apps: apps.map((app) => {
1100
1101
  const { remote, ...rest } = app
1101
1102
  return Object.assign(rest, { open: this.anywherePeerOpenPath(label, remote) })
@@ -1135,31 +1136,35 @@ class Server {
1135
1136
  if (location.origin !== new URL(base).origin) location = new URL(base)
1136
1137
  return { status: 302, headers: { Location: location.href, "Cache-Control": "no-store" } }
1137
1138
  }
1138
- // POST /anywhere/peers/<name>/apps/<launch|stop>: the peer's answer, an open URL turned into this node's open route
1139
- async relayAnywherePeerAction(name, action, body = {}) {
1139
+ // /anywhere/peers/<name>/<path>: the peer's own API through the daemon's proxy (API spec, Relays).
1140
+ // JSON routes get the peer's published addresses turned into this node's open routes, absolute,
1141
+ // since the client may be pinokio.co on another origin; an asset is passed through as it is.
1142
+ async relayAnywherePeer(name, method, relayPath, body = {}) {
1140
1143
  const anywhere = this.kernel.anywhere
1141
1144
  const peer = anywhere.peerFor(name)
1142
1145
  if (!peer) return { status: 400, body: { error: `Not a computer on your network: ${name}` } }
1143
1146
  const unavailable = this.anywherePeerUnavailable()
1144
1147
  if (unavailable) return { status: 502, body: { error: unavailable } }
1148
+ const json = relayPath.startsWith("anywhere/")
1145
1149
  let answer
1146
1150
  try {
1147
- answer = await anywhere.forwarders.request(`https://${peer.dnsName}/anywhere/apps/${action}`, {
1148
- method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body || {}), timeoutMs: 30000,
1149
- })
1151
+ answer = await anywhere.forwarders.request(`https://${peer.dnsName}/${relayPath}`, method === "POST"
1152
+ ? { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(body || {}), timeoutMs: 30000 }
1153
+ : { method, timeoutMs: 8000 })
1150
1154
  } catch (error) {
1151
1155
  return { status: 502, body: { error: `Could not reach ${peer.name}: ${error && error.message ? error.message : error}` } }
1152
1156
  }
1157
+ if (!json) return { status: answer.status, raw: answer.raw, contentType: answer.headers["content-type"] || "application/octet-stream" }
1153
1158
  let parsed
1154
1159
  try { parsed = JSON.parse(answer.body) } catch (_) {
1155
1160
  return { status: 502, body: { error: `${peer.name} did not answer with JSON (HTTP ${answer.status})` } }
1156
1161
  }
1157
- // only the peer's own published address becomes this node's open route; any other URL passes through
1158
- if (parsed && parsed.action === "open" && typeof parsed.url === "string") {
1159
- let host = null
1160
- try { host = new URL(parsed.url).hostname.toLowerCase() } catch (_) { }
1161
- if (host === peer.dnsName.toLowerCase()) parsed.url = this.anywherePeerOpenPath(peer.dnsName.split(".")[0], parsed.url)
1162
+ const own = (url) => { try { return new URL(url).hostname.toLowerCase() === peer.dnsName.toLowerCase() } catch (_) { return false } }
1163
+ const route = (url) => `http://127.0.0.1:${anywhere.serverPort()}${this.anywherePeerOpenPath(peer.dnsName.split(".")[0], url)}`
1164
+ if (parsed && Array.isArray(parsed.apps)) {
1165
+ for (const app of parsed.apps) if (app && typeof app.remote === "string" && own(app.remote)) app.remote = route(app.remote)
1162
1166
  }
1167
+ if (parsed && parsed.action === "open" && typeof parsed.url === "string" && own(parsed.url)) parsed.url = route(parsed.url)
1163
1168
  return { status: answer.status, body: parsed }
1164
1169
  }
1165
1170
  // the app's menu as launch targets: scripts and web addresses, never interface links;
@@ -7002,6 +7007,12 @@ class Server {
7002
7007
  this.kernel.vault.automaticScans.supported)
7003
7008
  next()
7004
7009
  })
7010
+ // pinokio.co's Remote tab reaches the peer routes from a browser on this computer; Chrome's
7011
+ // local network access wants this on the preflight, which the cors middleware below answers
7012
+ this.app.use("/anywhere/peers", (req, res, next) => {
7013
+ if (req.get("Access-Control-Request-Private-Network")) res.set("Access-Control-Allow-Private-Network", "true")
7014
+ next()
7015
+ })
7005
7016
  this.app.use(cors({
7006
7017
  origin: '*'
7007
7018
  }));
@@ -14512,7 +14523,8 @@ class Server {
14512
14523
  try {
14513
14524
  const answer = await fn(req)
14514
14525
  if (answer.headers) res.set(answer.headers)
14515
- if (answer.body === undefined) res.status(answer.status).end()
14526
+ if (answer.raw) res.status(answer.status).type(answer.contentType).send(answer.raw)
14527
+ else if (answer.body === undefined) res.status(answer.status).end()
14516
14528
  else res.status(answer.status).json(answer.body)
14517
14529
  } catch (error) {
14518
14530
  res.status(500).json({ error: error && error.message ? error.message : String(error) })
@@ -14520,8 +14532,13 @@ class Server {
14520
14532
  })
14521
14533
  this.app.get("/anywhere/peers", anywherePeerRoute(() => this.composeAnywherePeers()))
14522
14534
  this.app.get("/anywhere/peers/:name/open", anywherePeerRoute((req) => this.anywherePeerOpen(req.params.name, req.query)))
14523
- this.app.post("/anywhere/peers/:name/apps/launch", anywherePeerRoute((req) => this.relayAnywherePeerAction(req.params.name, "launch", req.body)))
14524
- this.app.post("/anywhere/peers/:name/apps/stop", anywherePeerRoute((req) => this.relayAnywherePeerAction(req.params.name, "stop", req.body)))
14535
+ // the peer's own API under its prefix: a client swaps the peer's address for this prefix and changes nothing else
14536
+ const anywhereRelay = (method, relayPath) => anywherePeerRoute((req) => this.relayAnywherePeer(req.params.name, method, relayPath(req), req.body))
14537
+ this.app.get("/anywhere/peers/:name/anywhere/status", anywhereRelay("GET", () => "anywhere/status"))
14538
+ this.app.get("/anywhere/peers/:name/anywhere/apps", anywhereRelay("GET", () => "anywhere/apps"))
14539
+ this.app.post("/anywhere/peers/:name/anywhere/apps/launch", anywhereRelay("POST", () => "anywhere/apps/launch"))
14540
+ this.app.post("/anywhere/peers/:name/anywhere/apps/stop", anywhereRelay("POST", () => "anywhere/apps/stop"))
14541
+ this.app.get("/anywhere/peers/:name/asset/*", anywhereRelay("GET", (req) => "asset/" + encodeURI(req.params[0])))
14525
14542
  const anywhereAction = (label, fn) => ex(async (req, res) => {
14526
14543
  const anywhere = this.kernel.anywhere
14527
14544
  if (!anywhere) {
@@ -517,7 +517,7 @@ document.addEventListener('DOMContentLoaded', function() {
517
517
  button.disabled = true
518
518
  othersNote = ''
519
519
  try {
520
- const response = await fetch(`/anywhere/peers/${encodeURIComponent(peer)}/apps/stop`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: button.dataset.stop }) })
520
+ const response = await fetch(`/anywhere/peers/${encodeURIComponent(peer)}/anywhere/apps/stop`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: button.dataset.stop }) })
521
521
  const answer = await response.json().catch(() => ({}))
522
522
  if (!response.ok) othersNote = `Could not stop it on ${peer}: ${answer.error || `HTTP ${response.status}`}`
523
523
  } catch (e) {
@@ -71,7 +71,7 @@ test('with none found the section says so, and a stop is relayed to the peer the
71
71
  stop.click()
72
72
  await new Promise((resolve) => setImmediate(resolve)); await new Promise((resolve) => setImmediate(resolve))
73
73
  const posted = h.state.calls.find((call) => call.method === 'POST')
74
- assert.deepEqual([posted.url, posted.body], ['/anywhere/peers/pinokio-1/apps/stop', '{"id":"comfy"}'])
74
+ assert.deepEqual([posted.url, posted.body], ['/anywhere/peers/pinokio-1/anywhere/apps/stop', '{"id":"comfy"}'])
75
75
  assert.equal(h.state.calls.filter((call) => call.url === '/anywhere/peers').length, 4, 'load, two polls, and once more after the stop')
76
76
  assert.equal(h.section().querySelector('.anywhere-error'), null)
77
77
  h.state.stopFails = true
@@ -43,7 +43,7 @@ test('peers lists the online computers that answered a status, with apps linked
43
43
  })
44
44
  const out = await Server.prototype.composeAnywherePeers.call(fake(anywhere))
45
45
  assert.equal(out.status, 200)
46
- assert.deepEqual(out.body, { peers: [{ name: 'pinokio-1', apps: [
46
+ assert.deepEqual(out.body, { peers: [{ name: 'pinokio-1', address: 'https://pinokio-1.tail1.ts.net', apps: [
47
47
  { id: 'comfy', name: 'ComfyUI', state: 'ready', exposed: true, open: '/anywhere/peers/pinokio-1/open?port=43000&to=%2Fui%2F%3Fa%3D1%23v' },
48
48
  { id: 'forge', name: 'Forge', state: 'starting', exposed: false, open: null },
49
49
  ] }] })
@@ -83,6 +83,28 @@ test('open validates the peer, the port and the path, then redirects to the forw
83
83
  assert.deepEqual(unavailable, { status: 502, body: { error: 'Turn Remote off and on to reach your other Pinokios' } }, 'a daemon from before the proxy existed')
84
84
  })
85
85
 
86
+ test('the relays carry the peer\'s API under its prefix, with its own addresses made absolute open routes', async () => {
87
+ const anywhere = anywhereWith({
88
+ 'https://pinokio-1.tail1.ts.net/anywhere/status': json({ phase: 'ready', apps: [
89
+ { id: 'comfy', name: 'ComfyUI', remote: 'https://pinokio-1.tail1.ts.net:43000/ui/?a=1#v' },
90
+ { id: 'ext', name: 'Ext', remote: 'https://example.com/' },
91
+ { id: 'forge', name: 'Forge', remote: null },
92
+ ] }),
93
+ 'https://pinokio-1.tail1.ts.net/anywhere/apps': json({ apps: [{ id: 'comfy', name: 'ComfyUI', icon: '/asset/api/comfy/icon.png' }] }),
94
+ 'https://pinokio-1.tail1.ts.net/asset/api/comfy/icon.png': async () => ({ status: 200, headers: { 'content-type': 'image/png' }, raw: Buffer.from([0x89, 0x50]), body: '' }),
95
+ })
96
+ const server = fake(anywhere)
97
+ const status = await Server.prototype.relayAnywherePeer.call(server, 'pinokio-1', 'GET', 'anywhere/status')
98
+ assert.equal(status.status, 200)
99
+ assert.deepEqual(status.body.apps.map((app) => app.remote), [
100
+ 'http://127.0.0.1:42000/anywhere/peers/pinokio-1/open?port=43000&to=%2Fui%2F%3Fa%3D1%23v', 'https://example.com/', null,
101
+ ], 'the peer\'s own address becomes an absolute open route; anything else passes through')
102
+ const catalog = await Server.prototype.relayAnywherePeer.call(server, 'pinokio-1', 'GET', 'anywhere/apps')
103
+ assert.deepEqual(catalog.body, { apps: [{ id: 'comfy', name: 'ComfyUI', icon: '/asset/api/comfy/icon.png' }] })
104
+ const icon = await Server.prototype.relayAnywherePeer.call(server, 'pinokio-1', 'GET', 'asset/api/comfy/icon.png')
105
+ assert.deepEqual([icon.status, icon.contentType, [...icon.raw]], [200, 'image/png', [0x89, 0x50]], 'an asset is passed through as bytes')
106
+ })
107
+
86
108
  test('launch and stop relay the peer\'s status and body, turning an open URL into the open route', async () => {
87
109
  const sent = []
88
110
  const anywhere = anywhereWith({
@@ -90,20 +112,20 @@ test('launch and stop relay the peer\'s status and body, turning an open URL int
90
112
  'https://pinokio-1.tail1.ts.net/anywhere/apps/stop': json({ error: 'Unknown app: x' }, 400),
91
113
  })
92
114
  const server = fake(anywhere)
93
- const launch = await Server.prototype.relayAnywherePeerAction.call(server, 'pinokio-1', 'launch', { id: 'comfy' })
94
- assert.deepEqual(launch, { status: 200, body: { action: 'open', url: '/anywhere/peers/pinokio-1/open?port=43000&to=%2F' } })
115
+ const launch = await Server.prototype.relayAnywherePeer.call(server, 'pinokio-1', 'POST', 'anywhere/apps/launch', { id: 'comfy' })
116
+ assert.deepEqual(launch, { status: 200, body: { action: 'open', url: 'http://127.0.0.1:42000/anywhere/peers/pinokio-1/open?port=43000&to=%2F' } })
95
117
  assert.equal(sent[0].method, 'POST')
96
118
  assert.equal(sent[0].body, '{"id":"comfy"}')
97
119
  assert.equal(sent[0].headers['Content-Type'], 'application/json')
98
- assert.deepEqual(await Server.prototype.relayAnywherePeerAction.call(server, 'pinokio-1', 'stop', { id: 'x' }), { status: 400, body: { error: 'Unknown app: x' } })
99
- assert.equal((await Server.prototype.relayAnywherePeerAction.call(server, 'nobody', 'stop', {})).status, 400)
120
+ assert.deepEqual(await Server.prototype.relayAnywherePeer.call(server, 'pinokio-1', 'POST', 'anywhere/apps/stop', { id: 'x' }), { status: 400, body: { error: 'Unknown app: x' } })
121
+ assert.equal((await Server.prototype.relayAnywherePeer.call(server, 'nobody', 'POST', 'anywhere/apps/stop', {})).status, 400)
100
122
  })
101
123
 
102
124
  test('a launch whose default is an outside address passes through unchanged', async () => {
103
125
  const anywhere = anywhereWith({
104
126
  'https://pinokio-1.tail1.ts.net/anywhere/apps/launch': json({ action: 'open', url: 'https://github.com/x/y' }),
105
127
  })
106
- const launch = await Server.prototype.relayAnywherePeerAction.call(fake(anywhere), 'pinokio-1', 'launch', { id: 'docs' })
128
+ const launch = await Server.prototype.relayAnywherePeer.call(fake(anywhere), 'pinokio-1', 'POST', 'anywhere/apps/launch', { id: 'docs' })
107
129
  assert.deepEqual(launch.body, { action: 'open', url: 'https://github.com/x/y' })
108
130
  })
109
131
 
@@ -113,12 +135,12 @@ test('a peer that cannot be reached or answers without JSON relays as 502', asyn
113
135
  'https://other.tail1.ts.net/anywhere/apps/stop': async () => ({ status: 404, headers: {}, body: '<html>' }),
114
136
  })
115
137
  const server = fake(anywhere)
116
- const down = await Server.prototype.relayAnywherePeerAction.call(server, 'pinokio-1', 'stop', { id: 'x' })
138
+ const down = await Server.prototype.relayAnywherePeer.call(server, 'pinokio-1', 'POST', 'anywhere/apps/stop', { id: 'x' })
117
139
  assert.equal(down.status, 502)
118
140
  assert.match(down.body.error, /Could not reach STUDIO: no answer/)
119
- const old = await Server.prototype.relayAnywherePeerAction.call(server, 'other', 'stop', { id: 'x' })
141
+ const old = await Server.prototype.relayAnywherePeer.call(server, 'other', 'POST', 'anywhere/apps/stop', { id: 'x' })
120
142
  assert.deepEqual(old, { status: 502, body: { error: 'other did not answer with JSON (HTTP 404)' } })
121
- const stopped = await Server.prototype.relayAnywherePeerAction.call(fake(anywhereWith({}, { proxy: false })), 'other', 'stop', {})
143
+ const stopped = await Server.prototype.relayAnywherePeer.call(fake(anywhereWith({}, { proxy: false })), 'other', 'POST', 'anywhere/apps/stop', {})
122
144
  assert.equal(stopped.status, 502)
123
145
  })
124
146
 
@@ -133,7 +155,9 @@ test('the peer routes answer a browser on this computer only, never a tailnet cl
133
155
  assert.equal(allowed(request({ remote: '::1', host: '[::1]:42000', origin: 'http://[::1]:42000' })), true)
134
156
  assert.equal(allowed(request({ remote: '::ffff:127.0.0.1', host: '127.0.0.1:42000', origin: 'http://127.0.0.1:42000' })), true)
135
157
  assert.equal(allowed(request({ origin: 'http://localhost:3000' })), false, 'another local site is not this page')
136
- assert.equal(allowed(request({ origin: 'https://pinokio.co' })), false)
158
+ assert.equal(allowed(request({ origin: 'https://pinokio.co' })), true, 'the Remote tab in a browser on this computer')
159
+ assert.equal(allowed(request({ origin: 'https://pinokio.co', headers: { 'X-Forwarded-For': '100.1.1.1' } })), false, 'but not through Serve')
160
+ assert.equal(allowed(request({ origin: 'https://pinokio.co.evil' })), false)
137
161
  assert.equal(allowed(request({ origin: 'not a url' })), false)
138
162
  assert.equal(allowed(request({ host: 'pinokio.tail1.ts.net' })), false, 'the node\'s own name')
139
163
  assert.equal(allowed(request({ headers: { 'X-Forwarded-For': '100.1.1.1' } })), false, 'through Serve with a forged loopback Host')