[verified] refactor: harden operations and redesign device console
This commit is contained in:
+17
-8
@@ -5,23 +5,32 @@ const BLOCKED_REQUEST_HEADERS = new Set([
|
||||
])
|
||||
const BLOCKED_RESPONSE_HEADERS = new Set([...BLOCKED_REQUEST_HEADERS, 'set-cookie', 'content-encoding'])
|
||||
|
||||
export class ProxyRequestError extends Error {
|
||||
constructor(code) {
|
||||
super(code === 'unsupported_content_type' ? 'unsupported proxy content type' : 'invalid proxy path')
|
||||
this.code = code
|
||||
}
|
||||
}
|
||||
|
||||
const invalidPath = () => new ProxyRequestError('invalid_path')
|
||||
|
||||
export function safeProxyPath(rest = '') {
|
||||
const raw = String(rest || '')
|
||||
if (!raw || raw.startsWith('/') || raw.includes('\\')) throw new Error('invalid proxy path')
|
||||
if (!raw || raw.startsWith('/') || raw.includes('\\')) throw invalidPath()
|
||||
let value = raw
|
||||
const assertSafe = candidate => {
|
||||
if (/[\\?#]/.test(candidate) || candidate.startsWith('//') || candidate.split('/').some(segment => segment === '..' || segment === '.')) throw new Error('invalid proxy path')
|
||||
if (/%(?:2f|5c|3f|23|2e)/i.test(candidate)) throw new Error('invalid proxy path')
|
||||
if (/[\\?#]/.test(candidate) || candidate.startsWith('//') || candidate.split('/').some(segment => segment === '..' || segment === '.')) throw invalidPath()
|
||||
if (/%(?:2f|5c|3f|23|2e)/i.test(candidate)) throw invalidPath()
|
||||
}
|
||||
for (let depth = 0; depth < 16; depth += 1) {
|
||||
assertSafe(value)
|
||||
let decoded
|
||||
try { decoded = decodeURIComponent(value) } catch { throw new Error('invalid proxy path') }
|
||||
try { decoded = decodeURIComponent(value) } catch { throw invalidPath() }
|
||||
if (decoded === value) return `/${raw}`
|
||||
value = decoded
|
||||
}
|
||||
// Refuse inputs whose semantics still change after a bounded number of decodes.
|
||||
throw new Error('invalid proxy path')
|
||||
throw invalidPath()
|
||||
}
|
||||
|
||||
export function proxyHeaders(input = {}) {
|
||||
@@ -37,17 +46,17 @@ export async function proxyToInstance({ client, request, reply, rest }) {
|
||||
const query = request.url.includes('?') ? request.url.slice(request.url.indexOf('?')) : ''
|
||||
const target = new URL(`${targetPath}${query}`, new URL(client.instance.url).origin)
|
||||
const origin = new URL(client.instance.url).origin
|
||||
if (target.origin !== origin) throw new Error('invalid proxy path')
|
||||
if (target.origin !== origin) throw invalidPath()
|
||||
const headers = proxyHeaders(request.headers)
|
||||
let body
|
||||
if (!['GET', 'HEAD'].includes(request.method)) {
|
||||
const type = (headers.get('content-type') || '').split(';')[0].trim().toLowerCase()
|
||||
if (request.body === undefined && !type) body = undefined
|
||||
else if (type.startsWith('multipart/')) { const error = new Error('unsupported proxy content type'); error.statusCode = 415; throw error }
|
||||
else if (type.startsWith('multipart/')) throw new ProxyRequestError('unsupported_content_type')
|
||||
else if (type === 'application/json' || type.endsWith('+json')) body = request.body === undefined ? undefined : JSON.stringify(request.body)
|
||||
else if (type.startsWith('text/') || type === 'application/x-www-form-urlencoded') body = request.body
|
||||
else if (Buffer.isBuffer(request.body)) body = request.body
|
||||
else { const error = new Error('unsupported proxy content type'); error.statusCode = 415; throw error }
|
||||
else throw new ProxyRequestError('unsupported_content_type')
|
||||
}
|
||||
const upstream = await client.request(target.toString(), { method: request.method, headers, body, redirect: 'manual' })
|
||||
reply.code(upstream.status)
|
||||
|
||||
Reference in New Issue
Block a user