chore: prepare GoodBuddy 0.8.2
Cross-platform packages / Validate source (push) Waiting to run
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, macos, macos-15) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, windows, windows-2025) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, linux, ubuntu-24.04) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, macos, macos-15-intel) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, windows, windows-2025) (push) Blocked by required conditions
Cross-platform packages / Publish GitHub Release (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, linux, ubuntu-24.04-arm) (push) Blocked by required conditions
Cross-platform packages / Validate source (push) Waiting to run
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, macos, macos-15) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, windows, windows-2025) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, linux, ubuntu-24.04) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, macos, macos-15-intel) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, windows, windows-2025) (push) Blocked by required conditions
Cross-platform packages / Publish GitHub Release (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, linux, ubuntu-24.04-arm) (push) Blocked by required conditions
This commit is contained in:
@@ -2,6 +2,11 @@ import { lookup as dnsLookup } from 'node:dns/promises'
|
||||
import { request as httpRequest } from 'node:http'
|
||||
import { isIP } from 'node:net'
|
||||
import { request as httpsRequest } from 'node:https'
|
||||
import { isIntranetCompatibilityEnabled } from '../intranet-compatibility-policy'
|
||||
import {
|
||||
isIntranetBrowserAddress,
|
||||
isPublicBrowserAddress
|
||||
} from '../browser/browser-url-policy'
|
||||
import { parseDocument, type ParsedDocument } from './document-parser'
|
||||
|
||||
type ResolvedAddress = {
|
||||
@@ -38,52 +43,28 @@ export type UrlImporterOptions = {
|
||||
}
|
||||
|
||||
const blockedHostnames = new Set([
|
||||
'localhost',
|
||||
'localhost.localdomain',
|
||||
'instance-data',
|
||||
'instance-data.ec2.internal',
|
||||
'metadata',
|
||||
'metadata.aws.internal',
|
||||
'metadata.google.internal'
|
||||
])
|
||||
|
||||
function isPrivateIpv4(address: string): boolean {
|
||||
const parts = address.split('.').map(Number)
|
||||
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part))) {
|
||||
return true
|
||||
}
|
||||
const [first = 0, second = 0] = parts
|
||||
return (
|
||||
first === 0 ||
|
||||
first === 10 ||
|
||||
first === 127 ||
|
||||
(first === 169 && second === 254) ||
|
||||
(first === 172 && second >= 16 && second <= 31) ||
|
||||
(first === 192 && second === 168) ||
|
||||
(first === 100 && second >= 64 && second <= 127) ||
|
||||
first >= 224
|
||||
)
|
||||
}
|
||||
|
||||
function isPrivateIpv6(address: string): boolean {
|
||||
const normalized = address.toLowerCase().split('%')[0] ?? ''
|
||||
if (
|
||||
normalized === '::' ||
|
||||
normalized === '::1' ||
|
||||
normalized.startsWith('fc') ||
|
||||
normalized.startsWith('fd') ||
|
||||
/^fe[89ab]/.test(normalized) ||
|
||||
normalized.startsWith('ff')
|
||||
) {
|
||||
return true
|
||||
}
|
||||
const mapped = normalized.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/)
|
||||
return mapped ? isPrivateIpv4(mapped[1] ?? '') : false
|
||||
}
|
||||
|
||||
export function isPublicAddress(address: string): boolean {
|
||||
const family = isIP(address)
|
||||
return family === 4
|
||||
? !isPrivateIpv4(address)
|
||||
: family === 6
|
||||
? !isPrivateIpv6(address)
|
||||
: false
|
||||
return isPublicBrowserAddress(address)
|
||||
}
|
||||
|
||||
export function isIntranetAddress(address: string): boolean {
|
||||
return isIntranetBrowserAddress(address)
|
||||
}
|
||||
|
||||
function addressClass(
|
||||
address: string
|
||||
): 'public' | 'intranet' | 'blocked' {
|
||||
if (isPublicAddress(address)) {
|
||||
return 'public'
|
||||
}
|
||||
return isIntranetAddress(address) ? 'intranet' : 'blocked'
|
||||
}
|
||||
|
||||
export function normalizeSourceUrl(input: string): URL {
|
||||
@@ -96,11 +77,19 @@ export function normalizeSourceUrl(input: string): URL {
|
||||
if (!['http:', 'https:'].includes(url.protocol)) {
|
||||
throw new Error('网页来源仅支持 HTTP(S)')
|
||||
}
|
||||
const hostname = url.hostname.toLowerCase().replace(/\.$/u, '')
|
||||
if (
|
||||
url.username ||
|
||||
url.password ||
|
||||
blockedHostnames.has(url.hostname.toLowerCase()) ||
|
||||
url.hostname.toLowerCase().endsWith('.localhost')
|
||||
blockedHostnames.has(hostname) ||
|
||||
(
|
||||
!isIntranetCompatibilityEnabled() &&
|
||||
(
|
||||
hostname === 'localhost' ||
|
||||
hostname === 'localhost.localdomain' ||
|
||||
hostname.endsWith('.localhost')
|
||||
)
|
||||
)
|
||||
) {
|
||||
throw new Error('该网页地址不允许导入')
|
||||
}
|
||||
@@ -211,15 +200,23 @@ export class UrlImporter {
|
||||
this.maximumRedirects = options.maximumRedirects ?? 5
|
||||
}
|
||||
|
||||
private async resolvePublic(url: URL): Promise<ResolvedAddress> {
|
||||
private async resolveAddress(url: URL): Promise<ResolvedAddress> {
|
||||
const addresses = await this.lookup(url.hostname)
|
||||
const address = addresses.find((candidate) =>
|
||||
isPublicAddress(candidate.address)
|
||||
const classes = addresses.map((candidate) =>
|
||||
candidate.family === isIP(candidate.address)
|
||||
? addressClass(candidate.address)
|
||||
: 'blocked'
|
||||
)
|
||||
const address = addresses[0]
|
||||
if (
|
||||
addresses.length === 0 ||
|
||||
addresses.some((candidate) => !isPublicAddress(candidate.address)) ||
|
||||
!address
|
||||
!address ||
|
||||
classes.includes('blocked') ||
|
||||
new Set(classes).size !== 1 ||
|
||||
(
|
||||
!isIntranetCompatibilityEnabled() &&
|
||||
classes.some((addressType) => addressType !== 'public')
|
||||
)
|
||||
) {
|
||||
throw new Error('网页地址解析到本机、私网或不可用地址')
|
||||
}
|
||||
@@ -232,7 +229,7 @@ export class UrlImporter {
|
||||
|
||||
for (let redirect = 0; redirect <= this.maximumRedirects; redirect += 1) {
|
||||
signal.throwIfAborted()
|
||||
const address = await this.resolvePublic(url)
|
||||
const address = await this.resolveAddress(url)
|
||||
response = await this.transport(
|
||||
url,
|
||||
address,
|
||||
|
||||
Reference in New Issue
Block a user