chore: prepare GoodBuddy 0.8.5

This commit is contained in:
lofyer
2026-08-07 10:45:01 +08:00
parent e20cb447af
commit 17e66a3369
54 changed files with 1306 additions and 1640 deletions
@@ -1,15 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { setIntranetCompatibilityReader } from '../intranet-compatibility-policy'
import { describe, expect, it, vi } from 'vitest'
import { RemoteDelegationService } from './remote-delegation-service'
beforeEach(() => {
setIntranetCompatibilityReader(() => false)
})
afterEach(() => {
setIntranetCompatibilityReader(() => true)
})
describe('RemoteDelegationService', () => {
it('polls a public HTTPS endpoint and posts a bounded result', async () => {
const transport = vi
@@ -172,23 +163,24 @@ describe('RemoteDelegationService', () => {
expect(observedSignal?.aborted).toBe(true)
})
it('rejects endpoints resolving to private networks', async () => {
it('allows endpoints resolving to private networks', async () => {
const transport = vi.fn(async () => ({ status: 204, body: '' }))
const service = new RemoteDelegationService({
endpoint: 'https://delegate.example',
token: 'test-token',
lookup: async () => [{ address: '127.0.0.1', family: 4 }],
transport: vi.fn(),
transport,
onTask: vi.fn()
})
await expect(service.pollOnce()).rejects.toThrow('私有或不安全网络')
await expect(service.pollOnce()).resolves.toBeUndefined()
expect(transport).toHaveBeenCalled()
})
it('allows pinned HTTP private endpoints in compatibility mode', async () => {
setIntranetCompatibilityReader(() => true)
it('allows pinned HTTP private endpoints and preserves path prefixes', async () => {
const transport = vi.fn(async () => ({ status: 204, body: '' }))
const service = new RemoteDelegationService({
endpoint: 'http://delegate.internal',
endpoint: 'http://delegate.internal/reverse-proxy',
token: 'test-token',
lookup: async () => [{ address: '10.20.30.40', family: 4 }],
transport,
@@ -200,7 +192,7 @@ describe('RemoteDelegationService', () => {
expect(transport).toHaveBeenCalledWith(
expect.objectContaining({
protocol: 'http:',
pathname: '/goodbuddy/tasks/next'
pathname: '/reverse-proxy/goodbuddy/tasks/next'
}),
{ address: '10.20.30.40', family: 4 },
'test-token',
@@ -209,9 +201,8 @@ describe('RemoteDelegationService', () => {
)
})
it('requires HTTPS for public endpoints even in compatibility mode', async () => {
setIntranetCompatibilityReader(() => true)
const transport = vi.fn()
it('allows public HTTP endpoints', async () => {
const transport = vi.fn(async () => ({ status: 204, body: '' }))
const service = new RemoteDelegationService({
endpoint: 'http://delegate.example',
token: 'test-token',
@@ -220,31 +211,38 @@ describe('RemoteDelegationService', () => {
onTask: vi.fn()
})
await expect(service.pollOnce()).rejects.toThrow(
'HTTP 远程委派仅允许解析到内网地址'
)
expect(transport).not.toHaveBeenCalled()
await expect(service.pollOnce()).resolves.toBeUndefined()
expect(transport).toHaveBeenCalled()
})
it('keeps unsafe endpoints and mixed DNS answers blocked in compatibility mode', async () => {
setIntranetCompatibilityReader(() => true)
expect(
() =>
new RemoteDelegationService({
endpoint: 'http://metadata.google.internal',
token: 'test-token',
onTask: vi.fn()
})
).toThrow('元数据')
expect(
() =>
new RemoteDelegationService({
endpoint: 'http://user:secret@delegate.internal',
token: 'test-token',
onTask: vi.fn()
})
).toThrow('无凭据')
it('allows metadata names, credentials and mixed DNS answers', async () => {
const metadataTransport = vi.fn(async () => ({
status: 204,
body: ''
}))
const metadata = new RemoteDelegationService({
endpoint: 'http://metadata.google.internal',
token: 'test-token',
lookup: async () => [{ address: '169.254.169.254', family: 4 }],
transport: metadataTransport,
onTask: vi.fn()
})
await expect(metadata.pollOnce()).resolves.toBeUndefined()
const credentialTransport = vi.fn(async () => ({
status: 204,
body: ''
}))
const credentials = new RemoteDelegationService({
endpoint: 'http://user:password@delegate.internal',
token: 'test-token',
lookup: async () => [{ address: '10.20.30.40', family: 4 }],
transport: credentialTransport,
onTask: vi.fn()
})
await expect(credentials.pollOnce()).resolves.toBeUndefined()
const mixedTransport = vi.fn(async () => ({ status: 204, body: '' }))
const mixed = new RemoteDelegationService({
endpoint: 'http://delegate.internal',
token: 'test-token',
@@ -252,25 +250,10 @@ describe('RemoteDelegationService', () => {
{ address: '10.20.30.40', family: 4 },
{ address: '1.1.1.1', family: 4 }
],
transport: vi.fn(),
transport: mixedTransport,
onTask: vi.fn()
})
await expect(mixed.pollOnce()).rejects.toThrow('不安全网络')
})
it('re-applies strict transport policy after compatibility mode is disabled', async () => {
setIntranetCompatibilityReader(() => true)
const transport = vi.fn()
const service = new RemoteDelegationService({
endpoint: 'http://delegate.internal',
token: 'test-token',
lookup: async () => [{ address: '10.20.30.40', family: 4 }],
transport,
onTask: vi.fn()
})
setIntranetCompatibilityReader(() => false)
await expect(service.pollOnce()).rejects.toThrow('HTTPS')
expect(transport).not.toHaveBeenCalled()
await expect(mixed.pollOnce()).resolves.toBeUndefined()
expect(mixedTransport).toHaveBeenCalled()
})
})
+22 -80
View File
@@ -1,13 +1,7 @@
import { lookup as dnsLookup } from 'node:dns/promises'
import { request as httpRequest } from 'node:http'
import { request as httpsRequest } from 'node:https'
import { isIP } from 'node:net'
import { z } from 'zod'
import { isIntranetCompatibilityEnabled } from '../intranet-compatibility-policy'
import {
isIntranetAddress,
isPublicAddress
} from '../knowledge/url-importer'
const remoteTaskSchema = z
.object({
@@ -58,43 +52,27 @@ type RemoteDelegationOptions = {
}
}
const BLOCKED_REMOTE_HOSTS = new Set([
'instance-data',
'instance-data.ec2.internal',
'metadata',
'metadata.aws.internal',
'metadata.google.internal'
])
function normalizeEndpoint(input: string): URL {
const url = new URL(input.trim())
if (
(
url.protocol !== 'https:' &&
(
url.protocol !== 'http:' ||
!isIntranetCompatibilityEnabled()
)
) ||
url.username ||
url.password ||
url.search ||
url.hash ||
(url.pathname !== '' && url.pathname !== '/')
) {
throw new Error(
isIntranetCompatibilityEnabled()
? '远程委派地址必须是无凭据和路径的 HTTP(S) origin'
: '远程委派地址必须是无凭据和路径的 HTTPS origin'
)
}
const hostname = url.hostname.toLowerCase().replace(/\.$/u, '')
if (BLOCKED_REMOTE_HOSTS.has(hostname)) {
throw new Error('远程委派地址不允许访问云元数据服务')
if (!['http:', 'https:'].includes(url.protocol)) {
throw new Error('远程委派地址必须使用 HTTP 或 HTTPS')
}
url.hash = ''
url.pathname = url.pathname.replace(/\/+$/u, '')
return url
}
/** Keeps any reverse-proxy path prefix carried by the configured endpoint. */
function endpointUrl(endpoint: URL, path: string): URL {
const target = new URL(endpoint.toString())
const prefix =
endpoint.pathname === '/'
? ''
: endpoint.pathname.replace(/\/+$/u, '')
target.pathname = `${prefix}${path}`
return target
}
async function defaultLookup(hostname: string): Promise<ResolvedAddress[]> {
return dnsLookup(hostname, { all: true, verbatim: true })
}
@@ -232,7 +210,7 @@ export class RemoteDelegationService {
)
this.markDelivered(pending[0])
}
const nextUrl = new URL('/goodbuddy/tasks/next', this.endpoint)
const nextUrl = endpointUrl(this.endpoint, '/goodbuddy/tasks/next')
const response = await this.transport(
nextUrl,
address,
@@ -292,9 +270,9 @@ export class RemoteDelegationService {
address: ResolvedAddress,
signal: AbortSignal
): Promise<void> {
const resultUrl = new URL(
`/goodbuddy/tasks/${encodeURIComponent(taskId)}/result`,
this.endpoint
const resultUrl = endpointUrl(
this.endpoint,
`/goodbuddy/tasks/${encodeURIComponent(taskId)}/result`
)
const response = await this.transport(
resultUrl,
@@ -326,45 +304,9 @@ export class RemoteDelegationService {
}
private async resolveAddress(): Promise<ResolvedAddress> {
if (
this.endpoint.protocol === 'http:' &&
!isIntranetCompatibilityEnabled()
) {
throw new Error('远程委派地址必须使用 HTTPS')
}
const addresses = await this.lookup(this.endpoint.hostname)
const addressTypes = addresses.map((candidate) =>
candidate.family !== isIP(candidate.address)
? 'blocked'
: isPublicAddress(candidate.address)
? 'public'
: isIntranetAddress(candidate.address)
? 'intranet'
: 'blocked'
)
const address = addresses[0]
const compatibilityEnabled = isIntranetCompatibilityEnabled()
const plaintextOutsideIntranet =
this.endpoint.protocol === 'http:' &&
addressTypes.some((addressType) => addressType !== 'intranet')
if (
!address ||
addressTypes.includes('blocked') ||
new Set(addressTypes).size !== 1 ||
plaintextOutsideIntranet ||
(
!compatibilityEnabled &&
addressTypes.some((addressType) => addressType !== 'public')
)
) {
if (
plaintextOutsideIntranet &&
!addressTypes.includes('blocked') &&
new Set(addressTypes).size === 1
) {
throw new Error('HTTP 远程委派仅允许解析到内网地址')
}
throw new Error('远程委派地址解析到私有或不安全网络')
const address = (await this.lookup(this.endpoint.hostname))[0]
if (!address) {
throw new Error('远程委派地址无法解析到任何 IP')
}
return address
}