fix: protect download bytes and recover local API errors

This commit is contained in:
Evandro Fonseca Junior committed 2026-09-30 17:08:35 -03:00
1 parent 039d25f084
commit e8a252ecde
9 files changed
+203 -184

No files matched your search

+51 -1
View File
@@ -1,4 +1,10 @@
import { describe, it, expect } from 'vitest'
import { describe, it, expect, vi } from 'vitest'
import * as os from 'node:os'
vi.mock('node:os', async importOriginal => ({
...(await importOriginal<typeof import('node:os')>()),
networkInterfaces: vi.fn().mockReturnValue({}),
}))
import {
isLoopbackHost,
validateBearerToken,
@@ -8,9 +14,47 @@ import {
isRequestBodySizeAllowed,
getLocalNetworkUrls,
validateSttBaseUrl,
sendInternalServerError,
} from './piagentui-server-core.js'
describe('PiAgentUi Server Core Security Helpers', () => {
describe('sendInternalServerError', () => {
const response = () => ({
headersSent: false,
writableEnded: false,
destroyed: false,
writeHead: vi.fn(),
end: vi.fn(),
destroy: vi.fn(),
})
it('sends a JSON error when the response has not started', () => {
const res = response()
sendInternalServerError(res)
expect(res.writeHead).toHaveBeenCalledWith(500, { 'Content-Type': 'application/json' })
expect(res.end).toHaveBeenCalledWith('{"error":"Internal Server Error"}')
expect(res.destroy).not.toHaveBeenCalled()
})
it('closes a partial response without writing another header', () => {
const res = { ...response(), headersSent: true }
sendInternalServerError(res)
expect(res.destroy).toHaveBeenCalledOnce()
expect(res.writeHead).not.toHaveBeenCalled()
expect(res.end).not.toHaveBeenCalled()
})
it('leaves completed and destroyed responses alone', () => {
for (const state of [{ writableEnded: true }, { destroyed: true }]) {
const res = { ...response(), ...state }
sendInternalServerError(res)
expect(res.writeHead).not.toHaveBeenCalled()
expect(res.end).not.toHaveBeenCalled()
expect(res.destroy).not.toHaveBeenCalled()
}
})
})
describe('isLoopbackHost', () => {
it('should accept loopback hostnames and IPs', () => {
expect(isLoopbackHost('localhost')).toBe(true)
@@ -84,6 +128,12 @@ describe('PiAgentUi Server Core Security Helpers', () => {
})
describe('getLocalNetworkUrls', () => {
it('preserves local access when network enumeration is unavailable', () => {
vi.mocked(os.networkInterfaces).mockImplementationOnce(() => {
throw new Error('network enumeration denied')
})
expect(getLocalNetworkUrls(58785)).toEqual([])
})
it('should build LAN URLs from non-internal IPv4 interfaces', () => {
const urls = getLocalNetworkUrls(58785, {
WiFi: [
+23 -4
View File
@@ -1,5 +1,19 @@
import * as path from 'node:path'
import * as os from 'node:os'
import type { ServerResponse } from 'node:http'
/** Finish a failed request without sending headers twice or leaving a partial response open. */
export function sendInternalServerError(
response: Pick<ServerResponse, 'headersSent' | 'writableEnded' | 'destroyed' | 'writeHead' | 'end' | 'destroy'>,
): void {
if (response.writableEnded || response.destroyed) return
if (response.headersSent) {
response.destroy()
return
}
response.writeHead(500, { 'Content-Type': 'application/json' })
response.end(JSON.stringify({ error: 'Internal Server Error' }))
}
/**
* Checks if a host string matches a loopback host (localhost or 127.0.0.1 or ::1)
@@ -123,10 +137,15 @@ export function validateHostAndOriginForAccess(
return true
}
export function getLocalNetworkUrls(
port: number,
interfaces: NodeJS.Dict<os.NetworkInterfaceInfo[]> = os.networkInterfaces(),
): string[] {
export function getLocalNetworkUrls(port: number, interfaces?: NodeJS.Dict<os.NetworkInterfaceInfo[]>): string[] {
if (!interfaces) {
try {
interfaces = os.networkInterfaces()
} catch {
// LAN discovery is best effort; local API access still works without it.
return []
}
}
const physicalUrls = new Set<string>()
const fallbackUrls = new Set<string>()
+2 -2
View File
@@ -12,6 +12,7 @@ import {
isRequestBodySizeAllowed,
getLocalNetworkUrls,
validateSttBaseUrl,
sendInternalServerError,
} from './piagentui-server-core.js'
import {
createPiAgentUiSettingsStore,
@@ -2075,8 +2076,7 @@ export default function (pi: ExtensionAPI) {
const urlPath = req.url || ''
if (urlPath.startsWith('/api/') || urlPath.startsWith('/global/')) {
handleApiRequest(req, res).catch(() => {
res.writeHead(500, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'Internal Server Error' }))
sendInternalServerError(res)
})
} else {
handleStaticFile(req, res, urlPath)