Add whois coreutil
This commit is contained in:
+1
-1
@@ -87,7 +87,7 @@ function bareOsEmitRaw(ctx, chunk) {
|
||||
return false
|
||||
}
|
||||
|
||||
var BARE_OS_HELP_BIN_SPACED = "agent arch awk baresay baretop base32 base64 basename basenc btop bundlebee cat chat chgrp chmod chown cksum clear cmp comm cp crontab curl cut date dd df diff dir dircolors dirname du echo edit env exit expand expr factor false find fmt fold getconf getfacl git git-pear grep groups hdms head help holesail hostid hostname hrpc iconv id install join journalctl jq kernel-boot-diff kernel-doctor kernel-explain kernel-fsck kernel-home-snapshot kernel-manifest-validate kernel-preflight kernel-triage kill link ln logger login logname logout ls man md5sum mkdir mkfifo mktemp mount mv nano nice nl nohup nproc numfmt od oidc-publish openssl openssl paste patch pathcap-verify pathchk pear-runtime-matrix pkg-swarm-index pr printenv printf procstat ps pwd readlink realpath rev rm rmdir savevault say sed seq setfacl sh sha1sum sha224sum sha256sum sha384sum sha512sum shuf sidecar sleep sort split ssh-keygen ssh-keygen sshd sshd stat sum sync systemctl tac tail tar tar tee test theme time timeout touch tr true truncate tsort tty ulimit umount uname unexpand uniq unlink uptime users vdir wc wget which who whoami xargs xattr yes"
|
||||
var BARE_OS_HELP_BIN_SPACED = "agent arch awk baresay baretop base32 base64 basename basenc btop bundlebee cat chat chgrp chmod chown cksum clear cmp comm cp crontab curl cut date dd df diff dir dircolors dirname du echo edit env exit expand expr factor false find fmt fold getconf getfacl git git-pear grep groups hdms head help holesail hostid hostname hrpc iconv id install join journalctl jq kernel-boot-diff kernel-doctor kernel-explain kernel-fsck kernel-home-snapshot kernel-manifest-validate kernel-preflight kernel-triage kill link ln logger login logname logout ls man md5sum mkdir mkfifo mktemp mount mv nano nice nl nohup nproc numfmt od oidc-publish openssl openssl paste patch pathcap-verify pathchk pear-runtime-matrix pkg-swarm-index pr printenv printf procstat ps pwd readlink realpath rev rm rmdir savevault say sed seq setfacl sh sha1sum sha224sum sha256sum sha384sum sha512sum shuf sidecar sleep sort split ssh-keygen ssh-keygen sshd sshd stat sum sync systemctl tac tail tar tar tee test theme time timeout touch tr true truncate tsort tty ulimit umount uname unexpand uniq unlink uptime users vdir wc wget which who whoami whois xargs xattr yes"
|
||||
async function run(ctx, argv) {
|
||||
ctx.console.log(
|
||||
'Bare OS — default user: guest | shell builtins: alias, barerc, cd, command, export, exit, login, logout, readonly, type, umask, unalias, unset, : | /bin: ' +
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
/* 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<string, unknown>} 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
|
||||
}
|
||||
|
||||
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) {
|
||||
const res = await fetchFn(url, { method: 'GET' })
|
||||
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
|
||||
}
|
||||
if (typeof fetch !== 'function') {
|
||||
ctx.console.error('whois: fetch is unavailable in this runtime')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
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(fetch, bootstrapUrl)
|
||||
let base = pickBaseUrlFromBootstrap(target.kind, target.query, bootstrap)
|
||||
if (!base && target.kind === 'ip' && isIpv6(target.query)) {
|
||||
const ipv6Bootstrap = await fetchJson(fetch, 'https://data.iana.org/rdap/ipv6.json')
|
||||
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(fetch, joinUrl(base, rdapPath))
|
||||
if (parsed.json) ctx.console.log(JSON.stringify(rdap, null, 2))
|
||||
else printSummary(ctx, target.kind, target.query, rdap)
|
||||
} catch (err) {
|
||||
ctx.console.error('whois: ' + (err && err.message ? err.message : String(err)))
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user