/* BARE_OS_BIN_API 1.0.0 — bump when staged /bin script semantics change (see developer guide). */ /** Shared helpers for drive-resident /bin scripts (prepended before each command). */ function bareStdin(ctx) { return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : '' } /** @param {number} mode @param {'file' | 'directory' | 'symlink'} type */ function bareFormatModeString(mode, type) { const typeChar = type === 'directory' ? 'd' : type === 'symlink' ? 'l' : '-' const perm = mode & 0o777 const r = (bit) => (perm & bit ? 'r' : '-') const w = (bit) => (perm & bit ? 'w' : '-') const x = (bit) => (perm & bit ? 'x' : '-') return ( typeChar + r(0o400) + w(0o200) + x(0o100) + r(0o040) + w(0o020) + x(0o010) + r(0o004) + w(0o002) + x(0o001) ) } /** @param {number} mtimeMs @param {number} [nowMs] */ function bareFormatLsMtime(mtimeMs, nowMs) { const now = nowMs != null ? nowMs : Date.now() const d = new Date(mtimeMs) const months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ] const mon = months[d.getMonth()] const day = String(d.getDate()).padStart(2, ' ') const sixMo = 180 * 24 * 3600 * 1000 if (Math.abs(now - mtimeMs) > sixMo) { const yr = String(d.getFullYear()).padStart(4, ' ') return mon + ' ' + day + ' ' + yr } const hh = String(d.getHours()).padStart(2, '0') const mm = String(d.getMinutes()).padStart(2, '0') return mon + ' ' + day + ' ' + hh + ':' + mm } /** @param {number} size */ function barePosixBlocks(size) { return Math.ceil(Number(size) / 512) || 0 } /** * Raw stdout for NUL/binary when **`process.stdout.write`** is missing. * If **`ctx.bareOsBinWrite(Uint8Array|string)`** is set (tests / host), use it. * @param {Record} ctx * @param {string | Uint8Array} chunk * @returns {boolean} */ function bareOsEmitRaw(ctx, chunk) { if (typeof ctx.bareOsBinWrite === 'function') { const b4 = ctx.b4a const u8 = typeof chunk === 'string' ? b4 && typeof b4.from === 'function' ? b4.from(chunk) : new TextEncoder().encode(chunk) : chunk ctx.bareOsBinWrite(u8 instanceof Uint8Array ? u8 : new Uint8Array(u8)) return true } const w = globalThis.process?.stdout?.write if (typeof w === 'function') { w.call(globalThis.process.stdout, chunk) return true } return false } /** Session env map (`vfs.env`, then `ctx.env`). Never throws. */ function bareOsEnv(ctx) { const v = ctx && ctx.vfs && ctx.vfs.env if (v && typeof v === 'object') return v const e = ctx && ctx.env if (e && typeof e === 'object') return e return {} } /** * Strict POSIX-ish decimal integer (no octal, no exponent, no empty). * @param {unknown} s * @returns {number} */ function bareOsParseDecInt(s) { const t = String(s == null ? '' : s).trim() if (!/^[+-]?(?:0|[1-9][0-9]*)$/.test(t)) return NaN const n = Number.parseInt(t, 10) return Number.isSafeInteger(n) ? n : NaN } /** @param {unknown} s */ function bareOsParseNonNegInt(s) { const n = bareOsParseDecInt(s) return n >= 0 ? n : NaN } /** * @param {Record} ctx * @param {string} name * @param {number} fallback * @param {number} [min] * @param {number} [max] */ function bareOsEnvInt(ctx, name, fallback, min, max) { const raw = bareOsEnv(ctx)[name] if (raw == null || raw === '') return fallback const n = Number.parseInt(String(raw), 10) if (!Number.isFinite(n)) return fallback let v = n if (min != null && v < min) v = min if (max != null && v > max) v = max return v } /** * @param {Record} ctx * @param {string} msg * @param {number} [code] */ function bareOsFail(ctx, msg, code) { if (msg) ctx.console.error(msg) ctx.exitCode = code == null ? 1 : code } /** @param {unknown} e */ function bareOsIsNotFoundErr(e) { const code = e && typeof e === 'object' ? e.code : '' if (code === 'ENOENT') return true const msg = String((e && e.message) || e || '') return /ENOENT|No such file|not found/i.test(msg) } /** * @param {Record} ctx * @param {unknown} buf * @returns {Uint8Array} */ function bareOsToU8(ctx, buf) { if (!buf) return new Uint8Array(0) if (buf instanceof Uint8Array) return buf if (ctx && ctx.b4a && typeof ctx.b4a.from === 'function') return ctx.b4a.from(buf) return new Uint8Array(buf) } /** @param {string} dir @param {string} name */ function bareOsJoinPath(dir, name) { const d = String(dir || '').replace(/\/+$/, '') const n = String(name || '').replace(/^\/+/, '') if (!d || d === '/') return '/' + n return d + '/' + n } /** @param {string} p */ function bareOsBaseName(p) { const t = String(p || '').replace(/\/+$/, '') if (!t || t === '/') return t === '/' ? '/' : '' const i = t.lastIndexOf('/') return i < 0 ? t : t.slice(i + 1) || t } /** @param {string} p */ function bareOsParentDir(p) { const t = String(p || '').replace(/\/+$/, '') || '/' if (t === '/') return '/' const i = t.lastIndexOf('/') return i <= 0 ? '/' : t.slice(0, i) || '/' } /** @param {string} p */ function bareOsNormPath(p) { return String(p || '').replace(/\/+$/, '') || '/' } /** * @param {Record} ctx * @param {string} p */ function bareOsResolvePath(ctx, p) { if (ctx && ctx.vfs && typeof ctx.vfs.resolveLogical === 'function') { try { return String(ctx.vfs.resolveLogical(p) || p) } catch { /* fall through */ } } return String(p || '') } /** * True when dest is src or lives under src (self-copy / self-move). * @param {Record} ctx * @param {string} src * @param {string} dest */ function bareOsDestInsideSrc(ctx, src, dest) { const s = bareOsNormPath(bareOsResolvePath(ctx, src)) const d = bareOsNormPath(bareOsResolvePath(ctx, dest)) if (s === d) return true if (s === '/') return d !== '/' return d === s || d.startsWith(s + '/') } const BARE_OS_B64_ALPH = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' /** @param {Uint8Array} u8 */ function bareOsB64Encode(u8) { let out = '' let i = 0 for (; i + 2 < u8.length; i += 3) { const n = (u8[i] << 16) | (u8[i + 1] << 8) | u8[i + 2] out += BARE_OS_B64_ALPH[(n >> 18) & 63] + BARE_OS_B64_ALPH[(n >> 12) & 63] + BARE_OS_B64_ALPH[(n >> 6) & 63] + BARE_OS_B64_ALPH[n & 63] } const rest = u8.length - i if (rest === 1) { const n = u8[i] << 16 out += BARE_OS_B64_ALPH[(n >> 18) & 63] + BARE_OS_B64_ALPH[(n >> 12) & 63] + '==' } else if (rest === 2) { const n = (u8[i] << 16) | (u8[i + 1] << 8) out += BARE_OS_B64_ALPH[(n >> 18) & 63] + BARE_OS_B64_ALPH[(n >> 12) & 63] + BARE_OS_B64_ALPH[(n >> 6) & 63] + '=' } return out } /** * RFC 4648 Base64 decode (also accepts URL-safe alphabet). Rejects junk. * @param {string} s * @returns {Uint8Array} */ function bareOsB64Decode(s) { const t = String(s).replace(/\s+/g, '') if (!t) return new Uint8Array(0) if (t.length % 4 === 1) throw new Error('invalid base64 length') let pad = 0 if (t.endsWith('==')) pad = 2 else if (t.endsWith('=')) pad = 1 const body = pad ? t.slice(0, t.length - pad) : t const bytes = [] let buf = 0 let bits = 0 for (let i = 0; i < body.length; i++) { const c = body[i] let v = BARE_OS_B64_ALPH.indexOf(c) if (v < 0) { if (c === '-') v = 62 else if (c === '_') v = 63 else throw new Error('invalid base64 character') } buf = (buf << 6) | v bits += 6 if (bits >= 8) { bits -= 8 bytes.push((buf >> bits) & 255) } } if (pad) { const want = Math.floor((body.length * 6) / 8) if (bytes.length > want) bytes.length = want } return new Uint8Array(bytes) } /** * @param {string} s * @returns {Uint8Array} */ function bareOsHexDecode(s) { const t = String(s).replace(/\s+/g, '') if (t.length % 2 !== 0) throw new Error('odd hex length') const out = new Uint8Array(t.length / 2) for (let i = 0; i < out.length; i++) { const pair = t.slice(i * 2, i * 2 + 2) if (!/^[0-9a-fA-F]{2}$/.test(pair)) throw new Error('invalid hex') out[i] = Number.parseInt(pair, 16) } return out } /** @param {Uint8Array} u8 */ function bareOsHexEncode(u8) { let s = '' for (let i = 0; i < u8.length; i++) s += u8[i].toString(16).padStart(2, '0') return s } function usage(ctx) { ctx.console.log( 'usage: whois [--json] TARGET\n' + 'Lookup domain, IP, or ASN information using RDAP.\n' + '\n' + ' --json print raw RDAP JSON\n' + ' -h, --help show this help' ) } function parseArgv(ctx, argv) { let json = false const positional = [] for (let i = 1; i < argv.length; i++) { const a = argv[i] if (a === '-h' || a === '--help') return { help: true } if (a === '--json') { json = true continue } if (a.startsWith('-')) { ctx.console.error('whois: unknown option ' + a) ctx.exitCode = 1 return null } positional.push(a) } if (positional.length !== 1) { ctx.console.error('whois: expected exactly one target') ctx.exitCode = 1 return null } return { help: false, json, target: positional[0].trim() } } function classifyTarget(raw) { const target = String(raw || '').trim() if (!target) return null if (/^AS\d+$/i.test(target)) { return { kind: 'asn', query: String(Number(target.slice(2))) } } if (/^\d+$/.test(target)) return { kind: 'asn', query: String(Number(target)) } if (isIpv4(target) || isIpv6(target)) return { kind: 'ip', query: target } if (isLikelyDomain(target)) return { kind: 'domain', query: target.toLowerCase() } return null } function isIpv4(s) { const parts = String(s).split('.') if (parts.length !== 4) return false for (const p of parts) { if (!/^\d+$/.test(p)) return false const n = Number(p) if (n < 0 || n > 255) return false } return true } function isIpv6(s) { const x = String(s) return x.includes(':') && /^[0-9a-fA-F:]+$/.test(x) } function isLikelyDomain(s) { const x = String(s).toLowerCase() if (x.length < 3 || x.length > 253) return false if (!x.includes('.')) return false if (/[^a-z0-9.-]/.test(x)) return false if (x.startsWith('.') || x.endsWith('.')) return false const labels = x.split('.') for (const label of labels) { if (!label || label.length > 63) return false if (label.startsWith('-') || label.endsWith('-')) return false } return true } async function fetchJson(fetchFn, url, timeoutMs) { const AC = typeof globalThis.AbortController === 'function' ? globalThis.AbortController : null let res if (AC) { const ac = new AC() const tid = setTimeout(() => ac.abort(), timeoutMs) try { res = await fetchFn(url, { method: 'GET', signal: ac.signal }) } finally { clearTimeout(tid) } } else { /** Minimal hosts may lack AbortController; race fetch against a timer (does not cancel TCP). */ let tid const abortErr = Object.assign(new Error('Aborted'), { name: 'AbortError' }) const deadline = new Promise((_, reject) => { tid = setTimeout(() => reject(abortErr), timeoutMs) }) try { res = await Promise.race([fetchFn(url, { method: 'GET' }), deadline]) } finally { if (tid) clearTimeout(tid) } } if (!res || !res.ok) { const status = res ? `${res.status} ${res.statusText || ''}`.trim() : 'unknown' throw new Error('HTTP error from ' + url + ': ' + status) } return await res.json() } function pickBaseUrlFromBootstrap(kind, query, bootstrap) { const services = Array.isArray(bootstrap && bootstrap.services) ? bootstrap.services : [] if (kind === 'domain') return pickDomainBaseUrl(query, services) if (kind === 'asn') return pickAsnBaseUrl(query, services) if (kind === 'ip') return pickIpBaseUrl(query, services) return null } function pickDomainBaseUrl(domain, services) { const labels = String(domain).toLowerCase().split('.') let best = null for (const entry of services) { const keys = Array.isArray(entry && entry[0]) ? entry[0] : [] const urls = Array.isArray(entry && entry[1]) ? entry[1] : [] const url = typeof urls[0] === 'string' ? urls[0] : null if (!url) continue for (const key of keys) { const tld = String(key || '').toLowerCase() if (!tld) continue if (labels[labels.length - 1] === tld && (!best || tld.length > best.key.length)) { best = { key: tld, url } } } } return best ? best.url : null } function pickAsnBaseUrl(asnString, services) { const asn = Number(asnString) if (!Number.isFinite(asn) || asn < 0) return null for (const entry of services) { const ranges = Array.isArray(entry && entry[0]) ? entry[0] : [] const urls = Array.isArray(entry && entry[1]) ? entry[1] : [] const url = typeof urls[0] === 'string' ? urls[0] : null if (!url) continue for (const r of ranges) { const m = String(r).match(/^(\d+)-(\d+)$/) if (!m) continue const lo = Number(m[1]) const hi = Number(m[2]) if (asn >= lo && asn <= hi) return url } } return null } function pickIpBaseUrl(ip, services) { const version = isIpv4(ip) ? 4 : 6 const ipBig = ipToBigInt(ip, version) if (ipBig == null) return null let best = null for (const entry of services) { const cidrs = Array.isArray(entry && entry[0]) ? entry[0] : [] const urls = Array.isArray(entry && entry[1]) ? entry[1] : [] const url = typeof urls[0] === 'string' ? urls[0] : null if (!url) continue for (const cidr of cidrs) { const parsed = parseCidr(String(cidr || '')) if (!parsed || parsed.version !== version) continue if (ipInCidr(ipBig, parsed)) { if (!best || parsed.prefix > best.prefix) best = { prefix: parsed.prefix, url } } } } return best ? best.url : null } function parseCidr(s) { const m = s.match(/^([^/]+)\/(\d+)$/) if (!m) return null const addr = m[1] const prefix = Number(m[2]) const version = isIpv4(addr) ? 4 : isIpv6(addr) ? 6 : 0 if (!version) return null const bits = version === 4 ? 32 : 128 if (prefix < 0 || prefix > bits) return null const base = ipToBigInt(addr, version) if (base == null) return null return { version, base, prefix, bits } } function ipInCidr(ipBig, cidr) { const shift = BigInt(cidr.bits - cidr.prefix) return (ipBig >> shift) === (cidr.base >> shift) } function ipToBigInt(ip, version) { if (version === 4) { const parts = ip.split('.').map((x) => Number(x)) if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n))) return null return ( (BigInt(parts[0]) << 24n) | (BigInt(parts[1]) << 16n) | (BigInt(parts[2]) << 8n) | BigInt(parts[3]) ) } const segments = expandIpv6(ip) if (!segments) return null let n = 0n for (const seg of segments) n = (n << 16n) | BigInt(seg) return n } function expandIpv6(ip) { const raw = String(ip) if (!raw.includes(':')) return null const halves = raw.split('::') if (halves.length > 2) return null const left = halves[0] ? halves[0].split(':') : [] const right = halves.length === 2 && halves[1] ? halves[1].split(':') : [] if (!allHex(left) || !allHex(right)) return null const missing = 8 - (left.length + right.length) if (missing < 0 || (halves.length === 1 && missing !== 0)) return null const full = [...left, ...Array(missing).fill('0'), ...right] if (full.length !== 8) return null return full.map((x) => Number.parseInt(x, 16)) } function allHex(parts) { for (const p of parts) { if (!p) return false if (!/^[0-9a-fA-F]{1,4}$/.test(p)) return false } return true } function joinUrl(base, suffix) { return String(base).replace(/\/+$/, '') + '/' + String(suffix).replace(/^\/+/, '') } function firstVCardValue(card, key) { const arr = Array.isArray(card) ? card : [] for (const row of arr) { if (!Array.isArray(row) || row.length < 4) continue if (String(row[0]).toLowerCase() === String(key).toLowerCase()) { return row[3] == null ? '' : String(row[3]) } } return '' } function collectRegistrar(entityList) { const entities = Array.isArray(entityList) ? entityList : [] for (const e of entities) { const roles = Array.isArray(e && e.roles) ? e.roles : [] if (!roles.includes('registrar')) continue const card = Array.isArray(e && e.vcardArray) ? e.vcardArray[1] : [] const fn = firstVCardValue(card, 'fn') const org = firstVCardValue(card, 'org') if (fn || org) return fn || org if (typeof e.handle === 'string' && e.handle) return e.handle } return '' } function printSummary(ctx, kind, query, rdap) { const lines = [] lines.push('Query: ' + query) lines.push('Type: ' + kind) if (rdap && rdap.objectClassName) lines.push('Object: ' + rdap.objectClassName) if (rdap && rdap.handle) lines.push('Handle: ' + rdap.handle) if (rdap && rdap.ldhName) lines.push('Name: ' + rdap.ldhName) else if (rdap && rdap.name) lines.push('Name: ' + rdap.name) if (rdap && rdap.country) lines.push('Country: ' + rdap.country) if (rdap && rdap.ipVersion) lines.push('IP Version: ' + rdap.ipVersion) if (rdap && rdap.startAddress && rdap.endAddress) { lines.push('Range: ' + rdap.startAddress + ' - ' + rdap.endAddress) } if (rdap && rdap.startAutnum != null && rdap.endAutnum != null) { lines.push('ASN Range: ' + rdap.startAutnum + ' - ' + rdap.endAutnum) } const registrar = collectRegistrar(rdap && rdap.entities) if (registrar) lines.push('Registrar: ' + registrar) const statuses = Array.isArray(rdap && rdap.status) ? rdap.status : [] if (statuses.length) lines.push('Status: ' + statuses.join(', ')) const nss = Array.isArray(rdap && rdap.nameservers) ? rdap.nameservers : [] if (nss.length) { const nsNames = nss .map((x) => (x && (x.ldhName || x.unicodeName) ? x.ldhName || x.unicodeName : '')) .filter(Boolean) if (nsNames.length) lines.push('Nameservers: ' + nsNames.join(', ')) } const events = Array.isArray(rdap && rdap.events) ? rdap.events : [] for (const e of events) { const action = String((e && e.eventAction) || '') const date = String((e && e.eventDate) || '') if (action === 'registration' && date) lines.push('Registered: ' + date) if (action === 'expiration' && date) lines.push('Expires: ' + date) if (action === 'last changed' && date) lines.push('Updated: ' + date) } for (const line of lines) ctx.console.log(line) } async function run(ctx, argv) { const parsed = parseArgv(ctx, argv) if (!parsed) return if (parsed.help) return usage(ctx) const target = classifyTarget(parsed.target) if (!target) { ctx.console.error('whois: unsupported target: ' + parsed.target) ctx.exitCode = 1 return } const fetchFn = typeof ctx.httpFetch === 'function' ? ctx.httpFetch : typeof fetch === 'function' ? fetch : null if (!fetchFn) { ctx.console.error('whois: fetch is unavailable in this runtime') ctx.exitCode = 1 return } /** Total wall-clock budget for bootstrap + optional ipv6 bootstrap + RDAP GET (shared across hops). */ const DEFAULT_RDAP_MS = 120000 const MAX_RDAP_MS = 600000 const timeoutRaw = Number.parseInt( String(ctx.vfs?.env?.BARE_OS_WHOIS_TIMEOUT_MS || String(DEFAULT_RDAP_MS)), 10 ) const totalBudgetMs = Number.isFinite(timeoutRaw) && timeoutRaw > 0 ? Math.min(timeoutRaw, MAX_RDAP_MS) : DEFAULT_RDAP_MS const deadlineMs = Date.now() + totalBudgetMs const hopTimeoutMs = () => Math.max(1, deadlineMs - Date.now()) let bootstrapUrl = '' let rdapPath = '' if (target.kind === 'domain') { bootstrapUrl = 'https://data.iana.org/rdap/dns.json' rdapPath = 'domain/' + encodeURIComponent(target.query) } else if (target.kind === 'ip') { bootstrapUrl = 'https://data.iana.org/rdap/ipv4.json' rdapPath = 'ip/' + encodeURIComponent(target.query) } else { bootstrapUrl = 'https://data.iana.org/rdap/asn.json' rdapPath = 'autnum/' + encodeURIComponent(target.query) } try { const bootstrap = await fetchJson(fetchFn, bootstrapUrl, hopTimeoutMs()) let base = pickBaseUrlFromBootstrap(target.kind, target.query, bootstrap) if (!base && target.kind === 'ip' && isIpv6(target.query)) { const ipv6Bootstrap = await fetchJson( fetchFn, 'https://data.iana.org/rdap/ipv6.json', hopTimeoutMs() ) base = pickBaseUrlFromBootstrap(target.kind, target.query, ipv6Bootstrap) } if (!base) { ctx.console.error('whois: no RDAP service found for target: ' + target.query) ctx.exitCode = 1 return } const rdap = await fetchJson(fetchFn, joinUrl(base, rdapPath), hopTimeoutMs()) if (parsed.json) ctx.console.log(JSON.stringify(rdap, null, 2)) else printSummary(ctx, target.kind, target.query, rdap) } catch (err) { const msg = String((err && err.message) || err || '') if (err && err.name === 'AbortError') { ctx.console.error( 'whois: network timeout (RDAP lookup exceeded ' + totalBudgetMs + 'ms)' ) } else if (/allowlist|denylist|HTTP blocked/i.test(msg)) { ctx.console.error('whois: blocked by HTTP policy: ' + msg) } else if (/ENOTFOUND|EAI_AGAIN|ENETUNREACH|EHOSTUNREACH/i.test(msg)) { ctx.console.error('whois: network unavailable or DNS failure') } else { ctx.console.error('whois: ' + msg) } ctx.exitCode = 1 } }