1058 lines
30 KiB
Plaintext
1058 lines
30 KiB
Plaintext
/* 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
|
|
}
|
|
|
|
/** 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<string, unknown>} 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<string, unknown>} 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<string, unknown>} 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<string, unknown>} 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<string, unknown>} 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
|
|
}
|
|
|
|
var TELNET_IAC = 255
|
|
var TELNET_DONT = 254
|
|
var TELNET_DO = 253
|
|
var TELNET_WONT = 252
|
|
var TELNET_WILL = 251
|
|
var TELNET_SB = 250
|
|
var TELNET_GA = 249
|
|
var TELNET_SE = 240
|
|
|
|
var TELNET_OPT_BINARY = 0
|
|
var TELNET_OPT_ECHO = 1
|
|
var TELNET_OPT_SGA = 3
|
|
var TELNET_OPT_STATUS = 5
|
|
var TELNET_OPT_TTYPE = 24
|
|
var TELNET_OPT_NAWS = 31
|
|
var TELNET_OPT_LINEMODE = 34
|
|
|
|
function telnetUsage() {
|
|
return (
|
|
'usage: telnet [--ipv4|--ipv6] [--connect-timeout-ms N] [--timeout-ms N] [--keepalive-ms N]\n' +
|
|
' [--reconnect N] [--reconnect-delay-ms N] [--naws-cols N] [--naws-rows N]\n' +
|
|
' [--ttype NAME] [--strict-close|--no-strict-close] host [port]\n' +
|
|
'\n' +
|
|
'escape command mode: Ctrl-] then one of: quit, close, reconnect, status, send <text>\n'
|
|
)
|
|
}
|
|
|
|
function bareTelnetOptionName(opt) {
|
|
switch (opt) {
|
|
case TELNET_OPT_BINARY:
|
|
return 'BINARY'
|
|
case TELNET_OPT_ECHO:
|
|
return 'ECHO'
|
|
case TELNET_OPT_SGA:
|
|
return 'SGA'
|
|
case TELNET_OPT_STATUS:
|
|
return 'STATUS'
|
|
case TELNET_OPT_TTYPE:
|
|
return 'TTYPE'
|
|
case TELNET_OPT_NAWS:
|
|
return 'NAWS'
|
|
case TELNET_OPT_LINEMODE:
|
|
return 'LINEMODE'
|
|
default:
|
|
return 'OPT_' + String(opt)
|
|
}
|
|
}
|
|
|
|
function bareTelnetCmdName(cmd) {
|
|
switch (cmd) {
|
|
case TELNET_DO:
|
|
return 'DO'
|
|
case TELNET_DONT:
|
|
return 'DONT'
|
|
case TELNET_WILL:
|
|
return 'WILL'
|
|
case TELNET_WONT:
|
|
return 'WONT'
|
|
case TELNET_SB:
|
|
return 'SB'
|
|
case TELNET_SE:
|
|
return 'SE'
|
|
case TELNET_GA:
|
|
return 'GA'
|
|
default:
|
|
return 'CMD_' + String(cmd)
|
|
}
|
|
}
|
|
|
|
function bareTelnetToU8(data) {
|
|
if (data instanceof Uint8Array) return data
|
|
if (typeof data === 'string') return new TextEncoder().encode(data)
|
|
if (Array.isArray(data)) return new Uint8Array(data.map((x) => Number(x) & 0xff))
|
|
if (data && typeof Buffer !== 'undefined' && typeof Buffer.isBuffer === 'function' && Buffer.isBuffer(data)) {
|
|
return new Uint8Array(data)
|
|
}
|
|
return new Uint8Array(0)
|
|
}
|
|
|
|
function bareTelnetU8Concat(parts) {
|
|
var total = 0
|
|
for (var i = 0; i < parts.length; i++) total += parts[i].length
|
|
var out = new Uint8Array(total)
|
|
var off = 0
|
|
for (var j = 0; j < parts.length; j++) {
|
|
out.set(parts[j], off)
|
|
off += parts[j].length
|
|
}
|
|
return out
|
|
}
|
|
|
|
function bareTelnetEncodeUtf8(s) {
|
|
return new TextEncoder().encode(String(s || ''))
|
|
}
|
|
|
|
function bareTelnetDecodeUtf8(u8) {
|
|
return new TextDecoder().decode(u8 || new Uint8Array(0))
|
|
}
|
|
|
|
function bareTelnetNewParseState() {
|
|
return {
|
|
mode: 'data',
|
|
cmd: 0,
|
|
sbOpt: -1,
|
|
sbData: []
|
|
}
|
|
}
|
|
|
|
function bareTelnetParseChunk(state, chunk) {
|
|
var st = state || bareTelnetNewParseState()
|
|
var dataIn = bareTelnetToU8(chunk)
|
|
var outData = []
|
|
var commands = []
|
|
for (var i = 0; i < dataIn.length; i++) {
|
|
var b = dataIn[i]
|
|
if (st.mode === 'data') {
|
|
if (b === TELNET_IAC) st.mode = 'iac'
|
|
else outData.push(b)
|
|
continue
|
|
}
|
|
if (st.mode === 'iac') {
|
|
if (b === TELNET_IAC) {
|
|
outData.push(TELNET_IAC)
|
|
st.mode = 'data'
|
|
} else if (b === TELNET_DO || b === TELNET_DONT || b === TELNET_WILL || b === TELNET_WONT) {
|
|
st.cmd = b
|
|
st.mode = 'negopt'
|
|
} else if (b === TELNET_SB) {
|
|
st.sbOpt = -1
|
|
st.sbData = []
|
|
st.mode = 'sbopt'
|
|
} else {
|
|
commands.push({ kind: 'cmd', cmd: b })
|
|
st.mode = 'data'
|
|
}
|
|
continue
|
|
}
|
|
if (st.mode === 'negopt') {
|
|
commands.push({ kind: 'neg', cmd: st.cmd, opt: b })
|
|
st.mode = 'data'
|
|
continue
|
|
}
|
|
if (st.mode === 'sbopt') {
|
|
st.sbOpt = b
|
|
st.sbData = []
|
|
st.mode = 'sbdata'
|
|
continue
|
|
}
|
|
if (st.mode === 'sbdata') {
|
|
if (b === TELNET_IAC) st.mode = 'sbiac'
|
|
else st.sbData.push(b)
|
|
continue
|
|
}
|
|
if (st.mode === 'sbiac') {
|
|
if (b === TELNET_IAC) {
|
|
st.sbData.push(TELNET_IAC)
|
|
st.mode = 'sbdata'
|
|
} else if (b === TELNET_SE) {
|
|
commands.push({
|
|
kind: 'sb',
|
|
opt: st.sbOpt,
|
|
data: new Uint8Array(st.sbData)
|
|
})
|
|
st.mode = 'data'
|
|
} else {
|
|
st.mode = 'data'
|
|
}
|
|
}
|
|
}
|
|
return { state: st, data: new Uint8Array(outData), commands: commands }
|
|
}
|
|
|
|
function bareTelnetEncodeNeg(cmd, opt) {
|
|
return new Uint8Array([TELNET_IAC, cmd, opt])
|
|
}
|
|
|
|
function bareTelnetEncodeSB(opt, payload) {
|
|
var p = bareTelnetToU8(payload)
|
|
var escaped = []
|
|
for (var i = 0; i < p.length; i++) {
|
|
escaped.push(p[i])
|
|
if (p[i] === TELNET_IAC) escaped.push(TELNET_IAC)
|
|
}
|
|
return new Uint8Array([TELNET_IAC, TELNET_SB, opt, ...escaped, TELNET_IAC, TELNET_SE])
|
|
}
|
|
|
|
function bareTelnetEncodeNAWS(cols, rows) {
|
|
var c = Math.min(Math.max(Number(cols) || 80, 0), 65535)
|
|
var r = Math.min(Math.max(Number(rows) || 24, 0), 65535)
|
|
return bareTelnetEncodeSB(TELNET_OPT_NAWS, new Uint8Array([(c >> 8) & 0xff, c & 0xff, (r >> 8) & 0xff, r & 0xff]))
|
|
}
|
|
|
|
function bareTelnetEncodeTTYPEIs(name) {
|
|
var n = String(name || 'bare-os').slice(0, 64)
|
|
var txt = bareTelnetEncodeUtf8(n)
|
|
return bareTelnetEncodeSB(TELNET_OPT_TTYPE, bareTelnetU8Concat([new Uint8Array([0]), txt]))
|
|
}
|
|
|
|
function bareTelnetMakeNego(opts) {
|
|
var o = opts || {}
|
|
return {
|
|
supportsLocal: new Set([TELNET_OPT_NAWS, TELNET_OPT_TTYPE, TELNET_OPT_SGA, TELNET_OPT_LINEMODE]),
|
|
supportsRemote: new Set([TELNET_OPT_ECHO, TELNET_OPT_SGA, TELNET_OPT_BINARY, TELNET_OPT_STATUS]),
|
|
localEnabled: new Set(),
|
|
remoteEnabled: new Set(),
|
|
nawsCols: Number(o.nawsCols) || 80,
|
|
nawsRows: Number(o.nawsRows) || 24,
|
|
ttype: String(o.ttype || 'bare-os')
|
|
}
|
|
}
|
|
|
|
function bareTelnetHandleNeg(nego, cmd, opt) {
|
|
var responses = []
|
|
var events = []
|
|
if (!nego) return { responses: responses, events: events }
|
|
if (cmd === TELNET_DO) {
|
|
if (nego.supportsLocal.has(opt)) {
|
|
if (!nego.localEnabled.has(opt)) {
|
|
nego.localEnabled.add(opt)
|
|
responses.push(bareTelnetEncodeNeg(TELNET_WILL, opt))
|
|
}
|
|
if (opt === TELNET_OPT_NAWS) responses.push(bareTelnetEncodeNAWS(nego.nawsCols, nego.nawsRows))
|
|
} else {
|
|
responses.push(bareTelnetEncodeNeg(TELNET_WONT, opt))
|
|
}
|
|
} else if (cmd === TELNET_DONT) {
|
|
if (nego.localEnabled.has(opt)) {
|
|
nego.localEnabled.delete(opt)
|
|
responses.push(bareTelnetEncodeNeg(TELNET_WONT, opt))
|
|
}
|
|
} else if (cmd === TELNET_WILL) {
|
|
if (nego.supportsRemote.has(opt)) {
|
|
if (!nego.remoteEnabled.has(opt)) {
|
|
nego.remoteEnabled.add(opt)
|
|
responses.push(bareTelnetEncodeNeg(TELNET_DO, opt))
|
|
}
|
|
if (opt === TELNET_OPT_ECHO) events.push({ type: 'remote_echo', enabled: true })
|
|
} else {
|
|
responses.push(bareTelnetEncodeNeg(TELNET_DONT, opt))
|
|
}
|
|
} else if (cmd === TELNET_WONT) {
|
|
if (nego.remoteEnabled.has(opt)) {
|
|
nego.remoteEnabled.delete(opt)
|
|
responses.push(bareTelnetEncodeNeg(TELNET_DONT, opt))
|
|
}
|
|
if (opt === TELNET_OPT_ECHO) events.push({ type: 'remote_echo', enabled: false })
|
|
}
|
|
events.push({ type: 'negotiation', cmd: bareTelnetCmdName(cmd), opt: bareTelnetOptionName(opt) })
|
|
return { responses: responses, events: events }
|
|
}
|
|
|
|
function bareTelnetHandleSB(nego, opt, data) {
|
|
var responses = []
|
|
var events = []
|
|
if (opt === TELNET_OPT_TTYPE) {
|
|
var u8 = bareTelnetToU8(data)
|
|
if (u8.length && u8[0] === 1) responses.push(bareTelnetEncodeTTYPEIs(nego.ttype))
|
|
events.push({ type: 'subnegotiation', opt: 'TTYPE' })
|
|
} else if (opt === TELNET_OPT_NAWS) {
|
|
events.push({ type: 'subnegotiation', opt: 'NAWS' })
|
|
} else if (opt === TELNET_OPT_LINEMODE) {
|
|
events.push({ type: 'subnegotiation', opt: 'LINEMODE' })
|
|
}
|
|
return { responses: responses, events: events }
|
|
}
|
|
|
|
function bareTelnetParseArgs(argv) {
|
|
var rest = (argv || []).slice(1)
|
|
var opts = {
|
|
family: 0,
|
|
connectTimeoutMs: 15000,
|
|
timeoutMs: 0,
|
|
keepaliveMs: 30000,
|
|
reconnect: 0,
|
|
reconnectDelayMs: 1000,
|
|
nawsCols: 80,
|
|
nawsRows: 24,
|
|
ttype: 'bare-os',
|
|
strictClose: true
|
|
}
|
|
var pos = []
|
|
for (var i = 0; i < rest.length; i++) {
|
|
var a = rest[i]
|
|
if (a === '--help' || a === '-h') return { help: true, opts: opts, host: '', port: 23 }
|
|
if (a === '--ipv4') {
|
|
opts.family = 4
|
|
continue
|
|
}
|
|
if (a === '--ipv6') {
|
|
opts.family = 6
|
|
continue
|
|
}
|
|
if (a === '--reconnect') {
|
|
opts.reconnect = Math.max(0, Number(rest[++i] || 1) || 0)
|
|
continue
|
|
}
|
|
if (a === '--connect-timeout-ms') {
|
|
opts.connectTimeoutMs = Math.max(100, Number(rest[++i] || 15000) || 15000)
|
|
continue
|
|
}
|
|
if (a === '--timeout-ms') {
|
|
opts.timeoutMs = Math.max(0, Number(rest[++i] || 0) || 0)
|
|
continue
|
|
}
|
|
if (a === '--keepalive-ms') {
|
|
opts.keepaliveMs = Math.max(0, Number(rest[++i] || 30000) || 0)
|
|
continue
|
|
}
|
|
if (a === '--reconnect-delay-ms') {
|
|
opts.reconnectDelayMs = Math.max(100, Number(rest[++i] || 1000) || 1000)
|
|
continue
|
|
}
|
|
if (a === '--naws-cols') {
|
|
opts.nawsCols = Math.max(1, Number(rest[++i] || 80) || 80)
|
|
continue
|
|
}
|
|
if (a === '--naws-rows') {
|
|
opts.nawsRows = Math.max(1, Number(rest[++i] || 24) || 24)
|
|
continue
|
|
}
|
|
if (a === '--ttype') {
|
|
opts.ttype = String(rest[++i] || 'bare-os')
|
|
continue
|
|
}
|
|
if (a === '--strict-close') {
|
|
opts.strictClose = true
|
|
continue
|
|
}
|
|
if (a === '--no-strict-close') {
|
|
opts.strictClose = false
|
|
continue
|
|
}
|
|
if (a.startsWith('-')) return { error: 'unknown option: ' + a }
|
|
pos.push(a)
|
|
}
|
|
if (!pos.length) return { error: 'missing host' }
|
|
var host = pos[0]
|
|
var port = pos[1] != null ? Number(pos[1]) : 23
|
|
if (!Number.isFinite(port) || port <= 0 || port > 65535) return { error: 'invalid port' }
|
|
return { opts: opts, host: host, port: Math.floor(port) }
|
|
}
|
|
|
|
function bareTelnetResolveConnector(ctx) {
|
|
if (typeof ctx.bareOsTelnetConnect === 'function') {
|
|
return function (host, port, opts) {
|
|
return ctx.bareOsTelnetConnect(host, port, opts || {})
|
|
}
|
|
}
|
|
var req = null
|
|
try {
|
|
if (typeof globalThis.require === 'function') req = globalThis.require
|
|
} catch {
|
|
req = null
|
|
}
|
|
if (!req && typeof Bare !== 'undefined' && Bare && typeof Bare.require === 'function') req = Bare.require
|
|
if (req) {
|
|
try {
|
|
var net = req('net')
|
|
if (net && typeof net.createConnection === 'function') {
|
|
return function (host, port, opts) {
|
|
return net.createConnection({
|
|
host: host,
|
|
port: port,
|
|
family: opts && opts.family ? opts.family : undefined
|
|
})
|
|
}
|
|
}
|
|
} catch {
|
|
/* fall through to syscall bridge */
|
|
}
|
|
}
|
|
if (typeof ctx.bareOsSyscall === 'function') {
|
|
return function (host, port, opts) {
|
|
const handlers = new Map()
|
|
let closed = false
|
|
let ended = false
|
|
let fd = -1
|
|
const family = opts && opts.family === 6 ? 10 : 2
|
|
const env = ctx && ctx.vfs && typeof ctx.vfs === 'object' ? ctx.vfs.env : null
|
|
if (env && typeof env === 'object') {
|
|
env.BARE_OS_POSIX_SOCKET_FD_BRIDGE = '1'
|
|
if (opts && Number(opts.connectTimeoutMs) > 0) {
|
|
env.BARE_OS_POSIX_SOCKET_CONNECT_TIMEOUT_MS = String(Math.floor(opts.connectTimeoutMs))
|
|
}
|
|
}
|
|
function emit(name, v) {
|
|
const arr = handlers.get(name) || []
|
|
for (const fn of arr) {
|
|
try {
|
|
fn(v)
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
}
|
|
function on(name, fn) {
|
|
const arr = handlers.get(name) || []
|
|
arr.push(fn)
|
|
handlers.set(name, arr)
|
|
}
|
|
function off(name, fn) {
|
|
const arr = handlers.get(name) || []
|
|
handlers.set(
|
|
name,
|
|
arr.filter((x) => x !== fn)
|
|
)
|
|
}
|
|
async function start() {
|
|
try {
|
|
const s = await ctx.bareOsSyscall('socket', { domain: family, type: 1, protocol: 0 })
|
|
if (!s || s.ok !== true || !Number.isFinite(s.fd)) {
|
|
throw new Error((s && s.note) || 'socket bridge unavailable')
|
|
}
|
|
fd = Number(s.fd)
|
|
const c = await ctx.bareOsSyscall('connect', { fd, host, port })
|
|
if (!c || c.ok !== true) {
|
|
throw new Error((c && (c.note || c.code)) || 'connect failed')
|
|
}
|
|
emit('connect')
|
|
while (!closed) {
|
|
const r = await ctx.bareOsSyscall('recv', { fd, len: 65536 })
|
|
if (!r || r.ok !== true) {
|
|
if (r && r.code === 'EAGAIN') {
|
|
await bareTelnetSleep(10)
|
|
continue
|
|
}
|
|
throw new Error((r && (r.note || r.code)) || 'recv failed')
|
|
}
|
|
const buf = bareTelnetToU8(r.buf)
|
|
if (buf.length) emit('data', buf)
|
|
if (r.eof || (Number(r.bytesReceived) === 0 && ended)) break
|
|
await bareTelnetSleep(1)
|
|
}
|
|
emit('end')
|
|
emit('close')
|
|
} catch (e) {
|
|
emit('error', e)
|
|
emit('close')
|
|
} finally {
|
|
if (fd >= 0) {
|
|
try {
|
|
await ctx.bareOsSyscall('close', { fd })
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
}
|
|
}
|
|
setTimeout(start, 0)
|
|
return {
|
|
on,
|
|
off,
|
|
removeListener: off,
|
|
setNoDelay() {},
|
|
setKeepAlive() {},
|
|
write(payload, cb) {
|
|
const p = bareTelnetToU8(payload)
|
|
ctx
|
|
.bareOsSyscall('send', { fd, buf: p })
|
|
.then((r) => {
|
|
if (!r || r.ok !== true) throw new Error((r && (r.note || r.code)) || 'send failed')
|
|
if (typeof cb === 'function') cb()
|
|
})
|
|
.catch((e) => {
|
|
emit('error', e)
|
|
if (typeof cb === 'function') cb(e)
|
|
})
|
|
return true
|
|
},
|
|
end() {
|
|
ended = true
|
|
if (fd >= 0) {
|
|
ctx.bareOsSyscall('shutdown', { fd, how: 1 }).catch(() => {})
|
|
}
|
|
},
|
|
destroy() {
|
|
closed = true
|
|
if (fd >= 0) {
|
|
ctx.bareOsSyscall('shutdown', { fd, how: 2 }).catch(() => {})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
function bareTelnetSleep(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0)))
|
|
}
|
|
|
|
function bareTelnetOnce(socket, okName, errName, timeoutMs) {
|
|
return new Promise((resolve, reject) => {
|
|
var t = null
|
|
function cleanup() {
|
|
if (t) clearTimeout(t)
|
|
if (socket && typeof socket.off === 'function') {
|
|
socket.off(okName, onOk)
|
|
socket.off(errName, onErr)
|
|
} else if (socket && typeof socket.removeListener === 'function') {
|
|
socket.removeListener(okName, onOk)
|
|
socket.removeListener(errName, onErr)
|
|
}
|
|
}
|
|
function onOk(v) {
|
|
cleanup()
|
|
resolve(v)
|
|
}
|
|
function onErr(e) {
|
|
cleanup()
|
|
reject(e || new Error(errName))
|
|
}
|
|
if (socket && typeof socket.on === 'function') {
|
|
socket.on(okName, onOk)
|
|
socket.on(errName, onErr)
|
|
}
|
|
if (timeoutMs > 0) {
|
|
t = setTimeout(function () {
|
|
cleanup()
|
|
reject(new Error('timeout waiting for ' + okName))
|
|
}, timeoutMs)
|
|
}
|
|
})
|
|
}
|
|
|
|
async function bareTelnetReadEscapeCommand(ctx, stdin, stdout) {
|
|
var ttyIn = stdin
|
|
if (ttyIn && typeof ttyIn.setRawMode === 'function' && ttyIn.isTTY) {
|
|
try {
|
|
ttyIn.setRawMode(false)
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
if (stdout && typeof stdout.write === 'function') stdout.write('\n')
|
|
if (typeof ctx.readLine === 'function') {
|
|
return String((await ctx.readLine('telnet> ')) || '').trim()
|
|
}
|
|
if (stdout && typeof stdout.write === 'function') stdout.write('telnet> ')
|
|
return ''
|
|
}
|
|
|
|
function bareTelnetWriteOut(out, text) {
|
|
if (out && typeof out.write === 'function') out.write(String(text))
|
|
}
|
|
|
|
async function bareTelnetRunSession(ctx, host, port, opts) {
|
|
var connector = bareTelnetResolveConnector(ctx)
|
|
if (!connector) {
|
|
ctx.console.error(
|
|
'telnet: no connector available (need ctx.bareOsTelnetConnect or net.createConnection runtime)'
|
|
)
|
|
ctx.exitCode = 127
|
|
return
|
|
}
|
|
var stdout = ctx.stdout || globalThis.process?.stdout
|
|
var stdin = ctx.stdin || globalThis.process?.stdin
|
|
var tries = 0
|
|
while (true) {
|
|
tries++
|
|
var socket = null
|
|
var parseState = bareTelnetNewParseState()
|
|
var nego = bareTelnetMakeNego(opts)
|
|
var ended = false
|
|
var sawData = false
|
|
var userClosed = false
|
|
var localRaw = false
|
|
var idleTimer = null
|
|
var keepTimer = null
|
|
try {
|
|
socket = connector(host, port, opts)
|
|
if (!socket || typeof socket.on !== 'function') throw new Error('connector did not return socket')
|
|
await bareTelnetOnce(socket, 'connect', 'error', opts.connectTimeoutMs)
|
|
if (typeof socket.setNoDelay === 'function') socket.setNoDelay(true)
|
|
if (typeof socket.setKeepAlive === 'function' && opts.keepaliveMs > 0) {
|
|
socket.setKeepAlive(true, opts.keepaliveMs)
|
|
}
|
|
if (opts.keepaliveMs > 0) {
|
|
keepTimer = setInterval(function () {
|
|
try {
|
|
socket.write(new Uint8Array([TELNET_IAC, TELNET_GA]))
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}, opts.keepaliveMs)
|
|
}
|
|
bareTelnetWriteOut(stdout, 'Connected to ' + host + ':' + port + '\n')
|
|
if (stdin && stdin.isTTY && typeof stdin.setRawMode === 'function') {
|
|
stdin.setRawMode(true)
|
|
localRaw = true
|
|
}
|
|
if (stdin && typeof stdin.resume === 'function') stdin.resume()
|
|
function resetIdle() {
|
|
if (idleTimer) clearTimeout(idleTimer)
|
|
if (opts.timeoutMs > 0) {
|
|
idleTimer = setTimeout(function () {
|
|
try {
|
|
socket.end()
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}, opts.timeoutMs)
|
|
}
|
|
}
|
|
resetIdle()
|
|
await new Promise((resolve, reject) => {
|
|
var onDataFromSocket = function (chunk) {
|
|
resetIdle()
|
|
var parsed = bareTelnetParseChunk(parseState, chunk)
|
|
parseState = parsed.state
|
|
if (parsed.data.length) bareTelnetWriteOut(stdout, bareTelnetDecodeUtf8(parsed.data))
|
|
if (parsed.data.length) sawData = true
|
|
for (var i = 0; i < parsed.commands.length; i++) {
|
|
var c = parsed.commands[i]
|
|
if (c.kind === 'neg') {
|
|
var r = bareTelnetHandleNeg(nego, c.cmd, c.opt)
|
|
for (var j = 0; j < r.responses.length; j++) socket.write(r.responses[j])
|
|
} else if (c.kind === 'sb') {
|
|
var sb = bareTelnetHandleSB(nego, c.opt, c.data)
|
|
for (var k = 0; k < sb.responses.length; k++) socket.write(sb.responses[k])
|
|
}
|
|
}
|
|
}
|
|
var onDataFromStdin = async function (chunk) {
|
|
resetIdle()
|
|
var u8 = bareTelnetToU8(chunk)
|
|
for (var i = 0; i < u8.length; i++) {
|
|
if (u8[i] === 29) {
|
|
var cmd = await bareTelnetReadEscapeCommand(ctx, stdin, stdout)
|
|
if (stdin && stdin.isTTY && typeof stdin.setRawMode === 'function') {
|
|
try {
|
|
stdin.setRawMode(true)
|
|
localRaw = true
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
if (cmd === 'quit' || cmd === 'close') {
|
|
ended = true
|
|
userClosed = true
|
|
try {
|
|
socket.end()
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
resolve()
|
|
return
|
|
}
|
|
if (cmd === 'status') {
|
|
bareTelnetWriteOut(stdout, 'status: connected=' + String(!ended) + ' attempt=' + String(tries) + '\n')
|
|
} else if (cmd === 'reconnect') {
|
|
ended = true
|
|
try {
|
|
socket.destroy()
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
resolve()
|
|
return
|
|
} else if (cmd.startsWith('send ')) {
|
|
var payload = cmd.slice(5)
|
|
socket.write(bareTelnetEncodeUtf8(payload))
|
|
}
|
|
return
|
|
}
|
|
}
|
|
socket.write(u8)
|
|
}
|
|
var onSocketEnd = function () {
|
|
ended = true
|
|
if (opts.strictClose && !userClosed && !sawData) {
|
|
reject(new Error('remote closed connection before interactive session'))
|
|
return
|
|
}
|
|
resolve()
|
|
}
|
|
var onSocketError = function (e) {
|
|
ended = true
|
|
reject(e || new Error('socket_error'))
|
|
}
|
|
socket.on('data', onDataFromSocket)
|
|
socket.on('end', onSocketEnd)
|
|
socket.on('close', onSocketEnd)
|
|
socket.on('error', onSocketError)
|
|
if (stdin && typeof stdin.on === 'function') stdin.on('data', onDataFromStdin)
|
|
})
|
|
} catch (e) {
|
|
var msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
|
ctx.console.error('telnet: ' + msg)
|
|
if (tries > opts.reconnect) {
|
|
ctx.exitCode = 1
|
|
if (localRaw && stdin && typeof stdin.setRawMode === 'function') {
|
|
try {
|
|
stdin.setRawMode(false)
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
return
|
|
}
|
|
} finally {
|
|
if (keepTimer) clearInterval(keepTimer)
|
|
if (idleTimer) clearTimeout(idleTimer)
|
|
if (socket) {
|
|
try {
|
|
socket.end()
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
if (localRaw && stdin && typeof stdin.setRawMode === 'function') {
|
|
try {
|
|
stdin.setRawMode(false)
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
}
|
|
if (tries > opts.reconnect) {
|
|
ctx.exitCode = 0
|
|
return
|
|
}
|
|
bareTelnetWriteOut(stdout, 'Reconnecting in ' + String(opts.reconnectDelayMs) + 'ms...\n')
|
|
await bareTelnetSleep(opts.reconnectDelayMs)
|
|
}
|
|
}
|
|
|
|
async function run(ctx, argv) {
|
|
var parsed = bareTelnetParseArgs(argv || [])
|
|
if (parsed.help) {
|
|
ctx.console.log(telnetUsage())
|
|
return
|
|
}
|
|
if (parsed.error) {
|
|
ctx.console.error('telnet: ' + parsed.error)
|
|
ctx.console.error(telnetUsage())
|
|
ctx.exitCode = 1
|
|
return
|
|
}
|
|
const envStrict = String(ctx.vfs?.env?.BARE_OS_TELNET_STRICT_CLOSE || '').trim()
|
|
if (envStrict === '0' || envStrict === 'false') parsed.opts.strictClose = false
|
|
if (envStrict === '1' || envStrict === 'true') parsed.opts.strictClose = true
|
|
await bareTelnetRunSession(ctx, parsed.host, parsed.port, parsed.opts)
|
|
}
|