[verified] refactor: harden operations and redesign device console
This commit is contained in:
+50
-7
@@ -1,14 +1,57 @@
|
||||
import { proxyToInstance } from './service.js'
|
||||
import { proxyToInstance, safeProxyPath, ProxyRequestError } from './service.js'
|
||||
import { defaultProxyPolicy } from './policy.js'
|
||||
import { ConfirmationStore, confirmationBinding } from './confirmations.js'
|
||||
|
||||
function requestTarget(request, rest) {
|
||||
const path = safeProxyPath(rest)
|
||||
const query = request.url.includes('?') ? request.url.slice(request.url.indexOf('?')) : ''
|
||||
return `${path}${query}`
|
||||
}
|
||||
|
||||
export function registerProxyRoutes(app, {
|
||||
findClient,
|
||||
proxy = proxyToInstance,
|
||||
policy = defaultProxyPolicy,
|
||||
configStore,
|
||||
confirmationStore = new ConfirmationStore(),
|
||||
}) {
|
||||
app.post('/api/proxy-confirmations/prepare', async (request, reply) => {
|
||||
try {
|
||||
const input = request.body || {}
|
||||
const client = findClient(input.instanceId, reply)
|
||||
if (!client) return
|
||||
const method = String(input.method || '').toUpperCase()
|
||||
const target = String(input.path || '')
|
||||
const parsed = new URL(target, 'http://confirmation.local')
|
||||
if (parsed.origin !== 'http://confirmation.local') throw new Error('invalid confirmation path')
|
||||
const decision = policy.authorize(method, parsed.pathname)
|
||||
if (!decision.allowed) return reply.code(decision.statusCode).send({ error: decision.reason })
|
||||
if (!decision.dangerous) return reply.code(400).send({ error: 'confirmation is only available for dangerous writes' })
|
||||
const binding = confirmationBinding({ instance: client.instance, revision: configStore.snapshot.revision, method, target, body: input.body })
|
||||
return confirmationStore.prepare(binding)
|
||||
} catch (error) { return reply.code(400).send({ error: error.message }) }
|
||||
})
|
||||
|
||||
export function registerProxyRoutes(app, { findClient, proxy = proxyToInstance }) {
|
||||
app.all('/api/proxy/:id/*', async (request, reply) => {
|
||||
const client = findClient(request.params.id, reply)
|
||||
if (!client) return
|
||||
try { return await proxy({ client, request, reply, rest: request.params['*'] }) }
|
||||
catch (error) {
|
||||
if (/invalid proxy path/.test(error.message)) return reply.code(400).send({ error: error.message })
|
||||
if (error.statusCode === 415) return reply.code(415).send({ error: error.message })
|
||||
throw error
|
||||
try {
|
||||
const target = requestTarget(request, request.params['*'])
|
||||
const pathname = new URL(target, 'http://proxy.local').pathname
|
||||
const decision = policy.authorize(request.method, pathname)
|
||||
if (!decision.allowed) return reply.code(decision.statusCode).send({ error: decision.reason })
|
||||
if (decision.dangerous) {
|
||||
const binding = confirmationBinding({ instance: client.instance, revision: configStore.snapshot.revision, method: request.method, target, body: request.body })
|
||||
const token = request.headers['x-confirmation-token']
|
||||
if (!token) return reply.code(428).send({ error: 'confirmation token required' })
|
||||
if (!confirmationStore.consume(token, binding)) return reply.code(403).send({ error: 'invalid, expired, replayed, or mismatched confirmation token' })
|
||||
}
|
||||
return await proxy({ client, request, reply, rest: request.params['*'] })
|
||||
} catch (error) {
|
||||
if (error instanceof ProxyRequestError && error.code === 'invalid_path') return reply.code(400).send({ error: 'invalid proxy path' })
|
||||
if (error instanceof ProxyRequestError && error.code === 'unsupported_content_type') return reply.code(415).send({ error: 'unsupported proxy content type' })
|
||||
request.log?.error?.({ err: error }, 'upstream proxy failed')
|
||||
return reply.code(502).send({ error: 'upstream request failed' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user