dsh 0.2.0-rc.2: in-container update (experimental channel), path-agnostic compose, upstream patches adapted to 0.1.x/0.2.x

This commit is contained in:
hermes committed 2026-10-01 19:21:19 +00:00
commit e6dcef9bf5
302 files changed
+75178

No files matched your search

@@ -0,0 +1,80 @@
import { lookup } from 'node:dns/promises'
import { isIP } from 'node:net'
import { domainToASCII } from 'node:url'
import { ProxyTransportError } from './transport.mjs'
const POSITIVE_TTL_MS = 60_000
const NEGATIVE_TTL_MS = 10_000
function normalizeHost(value) {
if (isIP(value) !== 0) return value.toLowerCase()
const ascii = domainToASCII(String(value).toLowerCase().replace(/\.+$/, ''))
if (ascii === '') throw new ProxyTransportError('target DNS name is invalid', { code: 'TARGET_DNS_FAILED' })
return ascii
}
function normalizeRecords(value) {
if (!Array.isArray(value)) value = [value]
const records = new Map()
for (const record of value) {
const address = typeof record === 'string' ? record : record?.address
const family = typeof record === 'object' ? Number(record?.family) : isIP(address)
if (typeof address !== 'string' || ![4, 6].includes(family) || isIP(address) !== family) continue
const ttl = typeof record?.ttl === 'number' && Number.isFinite(record.ttl)
? Math.max(0, Math.min(60, record.ttl))
: 60
records.set(`${family}:${address}`, Object.freeze({ address, family, ttl }))
}
return Object.freeze([...records.values()])
}
export class ProxyDnsCache {
constructor({ resolver = lookup, now = Date.now, positiveTtlMs = POSITIVE_TTL_MS, negativeTtlMs = NEGATIVE_TTL_MS } = {}) {
this.resolver = resolver
this.now = now
this.positiveTtlMs = positiveTtlMs
this.negativeTtlMs = negativeTtlMs
this.entries = new Map()
}
async resolve(host, revision, { signal } = {}) {
const normalized = normalizeHost(host)
const type = isIP(normalized)
if (type !== 0) return Object.freeze([{ address: normalized, family: type, ttl: 60 }])
const key = `${revision}\u0000${normalized}`
const cached = this.entries.get(key)
if (cached !== undefined && cached.expiresAt > this.now()) {
if (cached.error !== null) throw cached.error
return cached.records
}
if (signal?.aborted) throw new ProxyTransportError('target DNS resolution was cancelled', { code: 'REQUEST_CANCELLED', statusCode: 499 })
let records
try {
if (signal === undefined) records = await this.resolver(normalized, { all: true, verbatim: true })
else records = await new Promise((resolve, reject) => {
const cancelled = () => reject(new ProxyTransportError('target DNS resolution was cancelled', {
code: 'REQUEST_CANCELLED', statusCode: 499,
}))
signal.addEventListener('abort', cancelled, { once: true })
this.resolver(normalized, { all: true, verbatim: true }).then(resolve, reject).finally(() => {
signal.removeEventListener('abort', cancelled)
})
})
records = normalizeRecords(records)
if (records.length === 0) throw new Error('resolver returned no usable addresses')
} catch (cause) {
if (cause instanceof ProxyTransportError) throw cause
const error = new ProxyTransportError('target DNS resolution failed', { code: 'TARGET_DNS_FAILED', cause })
this.entries.set(key, Object.freeze({ records: null, error, expiresAt: this.now() + this.negativeTtlMs }))
throw error
}
const recordTtlMs = Math.min(...records.map(record => record.ttl * 1000), this.positiveTtlMs)
this.entries.set(key, Object.freeze({ records, error: null, expiresAt: this.now() + recordTtlMs }))
if (this.entries.size > 512) {
const now = this.now()
for (const [entryKey, entry] of this.entries) if (entry.expiresAt <= now) this.entries.delete(entryKey)
while (this.entries.size > 512) this.entries.delete(this.entries.keys().next().value)
}
return records
}
}