fix: protect download bytes and recover local API errors
This commit is contained in:
1 parent
039d25f084
commit
e8a252ecde
9 files changed
+203
-184
No files matched your search
@@ -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: [
|
||||
|
||||
@@ -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>()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user