1
0
mirror of https://github.com/20kdc/OC-KittenOS.git synced 2024-11-13 14:08:07 +11:00

Compare commits

..

4 Commits

Author SHA1 Message Date
20kdc
7d1f6d2cae A bit of terminal polish 2020-04-02 01:52:25 +01:00
20kdc
a585ce4a75 svc-t: Just realized will/won't need responses too 2020-04-02 00:15:36 +01:00
20kdc
3ed1cebe25 svc-t: Properly ignore TELNET subnegotiations 2020-04-02 00:08:28 +01:00
20kdc
7c70a1128c Bugfixes to Everest, finalize the terminal API 2020-04-01 23:24:20 +01:00
9 changed files with 470 additions and 198 deletions

View File

@ -55,7 +55,7 @@ return {
},
["neo-everest"] = {
desc = "KittenOS NEO / Everest (windowing)",
v = 5,
v = 9,
deps = {
"neo"
},
@ -68,7 +68,7 @@ return {
},
["neo-icecap"] = {
desc = "KittenOS NEO / Icecap",
v = 8,
v = 9,
deps = {
"neo"
},
@ -85,7 +85,7 @@ return {
},
["neo-secpolicy"] = {
desc = "KittenOS NEO / Secpolicy",
v = 8,
v = 9,
deps = {
},
dirs = {

View File

@ -3,12 +3,16 @@
local _, _, termId = ...
local ok = pcall(function ()
assert(string.sub(termId, 1, 8) == "x.svc.t/")
assert(string.sub(termId, 1, 12) == "x.neo.pub.t/")
end)
local termClose
if not ok then
termId = nil
neo.executeAsync("svc-t", function (res)
termId = res.access
termClose = res.close
neo.scheduleTimer(0)
end, "luashell")
while not termId do
@ -17,25 +21,105 @@ if not ok then
end
TERM = neo.requireAccess(termId, "terminal")
TERM.line("KittenOS NEO Lua Shell")
print = function (...)
local n = {}
local s = {...}
for i = 1, #s do
local v = s[i]
if v == nil then
v = "nil"
end
table.insert(n, tostring(v))
end
TERM.line(table.concat(n, " "))
end
-- Using event makes it easier for stuff
-- within the shell to not spectacularly explode.
event = require("event")(neo)
local alive = true
event.listen("k.procdie", function (_, _, pid)
if pid == TERM.pid then
alive = false
end
end)
run = function (x, ...)
local subPid = neo.executeAsync(x, ...)
TERM.write(([[
KittenOS NEO Shell Usage Notes
Prefixing = is an alias for 'return '.
io.read(): Reads a line.
print: 'print with table dumping' impl.
TERM: Your terminal. (see us-termi doc.)
os.execute(): launch terminal apps!
tries '*', 'sys-t-*', 'svc-t-*', 'app-*'
example: os.execute("luashell")
os.exit(): quit the shell
=listCmdApps(): -t- (terminal) apps
event: useful for setting up listeners
without breaking shell functionality
]]):gsub("[\r]*\n", "\r\n"))
function listCmdApps()
local apps = {}
for _, v in ipairs(neo.listApps()) do
if v:sub(4, 6) == "-t-" then
table.insert(apps, v)
end
end
return apps
end
local function vPrint(slike, ...)
local s = {...}
if #s > 1 then
for i = 1, #s do
if i ~= 1 then TERM.write("\t") end
vPrint(slike, s[i])
end
elseif slike and type(s[1]) == "string" then
TERM.write("\"" .. s[1] .. "\"")
elseif type(s[1]) ~= "table" then
TERM.write(tostring(s[1]))
else
TERM.write("{")
for k, v in pairs(s[1]) do
TERM.write("[")
vPrint(true, k)
TERM.write("] = ")
vPrint(true, v)
TERM.write(", ")
end
TERM.write("}")
end
end
print = function (...)
vPrint(false, ...)
TERM.write("\r\n")
end
local ioBuffer = ""
io = {
read = function ()
while alive do
local pos = ioBuffer:find("\n")
if pos then
local line = ioBuffer:sub(1, pos):gsub("\r", "")
ioBuffer = ioBuffer:sub(pos + 1)
return line
end
local e = {event.pull()}
if e[1] == TERM.id then
if e[2] == "data" then
ioBuffer = ioBuffer .. e[3]
end
end
end
end,
write = function (s) TERM.write(s) end
}
local originalOS = os
os = setmetatable({}, {
__index = originalOS
})
function os.exit()
alive = false
end
function os.execute(x, ...)
local subPid = neo.executeAsync(x, TERM.id, ...)
if not subPid then
subPid = neo.executeAsync("sys-t-" .. x, TERM.id, ...)
end
@ -49,40 +133,32 @@ run = function (x, ...)
error("cannot find " .. x)
end
while true do
local e = {coroutine.yield()}
local e = {event.pull()}
if e[1] == "k.procdie" then
if e[3] == TERM.pid then
alive = false
return
elseif e[3] == subPid then
if e[3] == subPid then
return
end
end
end
end
exit = function ()
alive = false
end
while alive do
local e = {coroutine.yield()}
if e[1] == "k.procdie" then
if e[3] == TERM.pid then
alive = false
end
elseif e[1] == TERM.id then
if e[2] == "line" then
TERM.line("> " .. e[3])
local ok, err = pcall(function ()
if e[3]:sub(1, 1) == "=" then
e[3] = "return " .. e[3]:sub(2)
end
print(assert(load(e[3]))())
end)
if not ok then
TERM.line(tostring(err))
TERM.write("> ")
local code = io.read()
if code then
local ok, err = pcall(function ()
if code:sub(1, 1) == "=" then
code = "return " .. code:sub(2)
end
print(assert(load(code))())
end)
if not ok then
TERM.write(tostring(err) .. "\r\n")
end
end
end
end
if termClose then
termClose()
end

View File

@ -15,66 +15,224 @@ local function rW()
return string.format("%04x", math.random(0, 65535))
end
local id = "svc.t/" .. rW() .. rW() .. rW() .. rW()
local id = "neo.pub.t/" .. rW() .. rW() .. rW() .. rW()
local closeNow = false
-- Terminus Registration State --
local tReg = neo.requireAccess("r." .. id, "registration")
local sendSigs = {}
-- unicode.safeTextFormat'd lines
-- Display State --
-- unicode.safeTextFormat'd lines.
-- The size of this must not go below 1.
local console = {}
-- This must not go below 3.
local conW = 40
local conCX, conCY = 1, 1
for i = 1, 14 do
console[i] = (" "):rep(40)
console[i] = (" "):rep(conW)
end
local l15 = ""
--++++++++++++++++++++++++++++++++++++++++
-- sW must not go below 3.
-- sH must not go below 2.
local sW, sH = 40, 15
local cX = 1
local windows = neo.requireAccess("x.neo.pub.window", "windows")
local window = windows(sW, sH, title)
local function fmtLine(s)
s = unicode.safeTextFormat(s)
local l = unicode.len(s)
return unicode.sub(s .. (" "):rep(sW - l), -sW)
-- Line Editing State --
-- Nil if line editing is off.
-- In this case, the console height
-- must be adjusted accordingly.
local leText = ""
-- These are NOT nil'd out,
-- particularly not the history buffer.
local leCX = 1
local leHistory = {
-- Size = history buffer size
"", "", "", ""
}
local function cycleHistoryUp()
local backupFirst = leHistory[1]
for i = 1, #leHistory - 1 do
leHistory[i] = leHistory[i + 1]
end
leHistory[#leHistory] = backupFirst
end
local function cycleHistoryDown()
local backup = leHistory[1]
for i = 2, #leHistory do
backup, leHistory[i] = leHistory[i], backup
end
leHistory[1] = backup
end
local function line(i)
local l, c = console[i] or l15
l, c = unicode.safeTextFormat(l, cX)
l = require("lineedit").draw(sW, l, i == sH and c)
if i ~= sH then
window.span(1, i, l, 0xFFFFFF, 0)
-- Window --
local window = neo.requireAccess("x.neo.pub.window", "window")(conW, #console + 1, title)
-- Core Terminal Functions --
local function setSize(w, h)
conW = w
while #console < h do
table.insert(console, "")
end
while #console > h do
table.remove(console, 1)
end
for i = 1, #console do
console[i] = unicode.sub(console[i], 1, w) .. (" "):rep(w - unicode.len(console[i]))
end
if leText then
window.setSize(w, h + 1)
else
window.span(1, i, l, 0, 0xFFFFFF)
window.setSize(w, h)
end
conCX, conCY = 1, h
end
local function setLineEditing(state)
if state and not leText then
leText = ""
leCX = 1
setSize(conW, #console)
elseif leText and not state then
leText = nil
setSize(conW, #console)
end
end
local function incoming(s)
local function shift(f)
local function draw(i)
if console[i] then
window.span(1, i, console[i], 0, 0xFFFFFF)
elseif leText then
window.span(1, i, require("lineedit").draw(conW, unicode.safeTextFormat(leText, leCX)), 0xFFFFFF, 0)
end
end
local function drawDisplay()
for i = 1, #console do draw(i) end
end
-- Terminal Visual --
local function writeFF()
if conCY ~= #console then
conCY = conCY + 1
else
for i = 1, #console - 1 do
console[i] = console[i + 1]
end
console[#console] = f
end
-- Need to break this safely.
shift("")
for i = 1, unicode.len(s) do
local ch = unicode.sub(s, i, i)
if unicode.wlen(console[#console] .. ch) > sW then
shift(" ")
end
console[#console] = console[#console] .. ch
end
for i = 1, #console do
line(i)
console[#console] = (" "):rep(conW)
end
end
local sendSigs = {}
local function writeData(data)
-- handle data until completion
while #data > 0 do
local char = unicode.sub(data, 1, 1)
data = unicode.sub(data, 2)
-- handle character
if char == "\t" then
-- not ideal, but allowed
char = " "
end
if char == "\r" then
conCX = 1
elseif char == "\n" then
conCX = 1
writeFF()
elseif char == "\a" then
-- Bell (er...)
elseif char == "\b" then
conCX = math.max(1, conCX - 1)
elseif char == "\v" or char == "\f" then
writeFF()
else
local charL = unicode.wlen(char)
if (conCX + charL - 1) > conW then
conCX = 1
writeFF()
end
local spaces = (" "):rep(charL - 1)
console[conCY] = unicode.sub(console[conCY], 1, conCX - 1) .. char .. spaces .. unicode.sub(console[conCY], conCX + charL)
conCX = conCX + charL
end
end
end
-- The Terminus --
local tvBuildingCmd = ""
local tvBuildingUTF = ""
local tvSubnegotiation = false
local function incoming(s)
tvBuildingCmd = tvBuildingCmd .. s
-- Flush Cmd
while #tvBuildingCmd > 0 do
if tvBuildingCmd:byte() == 255 then
-- It's a command. Uhoh.
if #tvBuildingCmd < 2 then break end
local cmd = tvBuildingCmd:byte(2)
local param = tvBuildingCmd:byte(3)
local cmdLen = 2
-- Command Lengths
if cmd >= 251 and cmd <= 254 then cmdLen = 3 end
if #tvBuildingCmd < cmdLen then break end
if cmd == 240 then
-- SE
tvSubnegotiation = false
elseif cmd == 250 then
-- SB
tvSubnegotiation = true
elseif cmd == 251 and param == 1 then
-- WILL ECHO (respond with DO ECHO, disable line editing)
-- test using io.write("\xFF\xFB\x01")
for _, v in pairs(sendSigs) do
v("telnet", "\xFF\xFD\x01")
end
setLineEditing(false)
elseif cmd == 252 and param == 1 then
-- WON'T ECHO (respond with DON'T ECHO, enable line editing)
for _, v in pairs(sendSigs) do
v("telnet", "\xFF\xFE\x01")
end
setLineEditing(true)
elseif cmd == 251 or cmd == 252 then
-- WILL/WON'T (x) (respond with DON'T (X))
local res = "\xFF\xFE" .. string.char(param)
for _, v in pairs(sendSigs) do
v("telnet", res)
end
elseif cmd == 253 or cmd == 254 then
-- DO/DON'T (x) (respond with WON'T (X))
local res = "\xFF\xFC" .. string.char(param)
for _, v in pairs(sendSigs) do
v("telnet", res)
end
elseif cmd == 255 then
if not tvSubnegotiation then
tvBuildingUTF = tvBuildingUTF .. "\xFF"
end
end
tvBuildingCmd = tvBuildingCmd:sub(cmdLen + 1)
else
if not tvSubnegotiation then
tvBuildingUTF = tvBuildingUTF .. tvBuildingCmd:sub(1, 1)
end
tvBuildingCmd = tvBuildingCmd:sub(2)
end
end
-- Flush UTF
while #tvBuildingUTF > 0 do
local head = tvBuildingUTF:byte()
local len = 1
if head >= 192 and head < 224 then len = 2 end
if head >= 224 and head < 240 then len = 3 end
if head >= 240 and head < 248 then len = 4 end
if head >= 248 and head < 252 then len = 5 end
if head >= 252 and head < 254 then len = 6 end
if #tvBuildingUTF < len then break end
-- verified one full character...
local char = tvBuildingUTF:sub(1, len)
tvBuildingUTF = tvBuildingUTF:sub(len + 1)
writeData(char)
end
end
do
tReg(function (_, pid, sendSig)
@ -82,11 +240,12 @@ do
return {
id = "x." .. id,
pid = neo.pid,
line = function (text)
write = function (text)
incoming(tostring(text))
drawDisplay()
end
}
end)
end, true)
if retTbl then
coroutine.resume(coroutine.create(retTbl), {
@ -99,55 +258,72 @@ do
end
end
-- This decides the history buffer size.
local history = {
"", "", "", ""
}
local function cycleHistoryUp()
local backupFirst = history[1]
for i = 1, #history - 1 do
history[i] = history[i + 1]
end
history[#history] = backupFirst
end
local function cycleHistoryDown()
local backup = history[1]
for i = 2, #history do
backup, history[i] = history[i], backup
end
history[1] = backup
end
local control = false
local function key(a, c)
if c == 200 then
-- History cursor up (history down)
l15 = history[#history]
cX = 1
cycleHistoryDown()
return
elseif c == 208 then
-- History cursor down (history up)
l15 = history[#history]
cX = 1
cycleHistoryUp()
return
end
local lT, lC, lX = require("lineedit").key(a, c, l15, cX)
l15 = lT or l15
cX = lC or cX
if lX == "nl" then
cycleHistoryUp()
history[#history] = l15
for _, v in pairs(sendSigs) do
v("line", l15)
if control then
if e[5] == 203 and conW > 8 then
setSize(conW - 1, #console)
return
elseif e[5] == 200 and #console > 1 then
setSize(conW, #console - 1)
return
elseif e[5] == 205 then
setSize(conW + 1, #console)
return
elseif e[5] == 208 then
setSize(conW, #console + 1)
return
end
end
-- so with the reserved ones dealt with...
if not leText then
-- Line Editing not active.
-- For now support a bare minimum.
for _, v in pairs(sendSigs) do
if control then
if a == 99 then
v("telnet", "\xFF\xF4")
end
elseif a == "\r" then
v("data", "\r\n")
elseif a then
v("data", a)
end
end
elseif not control then
-- Line Editing active and control isn't involved
if c == 200 or c == 208 then
-- History cursor up (history down)
leText = leHistory[#leHistory]
leCX = unicode.len(leText) + 1
if c == 208 then
cycleHistoryUp()
else
cycleHistoryDown()
end
return
end
local lT, lC, lX = require("lineedit").key(a, c, leText, leCX)
leText = lT or leText
leCX = lC or leCX
if lX == "nl" then
cycleHistoryUp()
leHistory[#leHistory] = leText
-- the whole thing {
local fullText = leText .. "\r\n"
writeData(fullText)
drawDisplay()
for _, v in pairs(sendSigs) do
v("data", fullText)
end
-- }
leText = ""
leCX = 1
end
l15 = ""
cX = 1
end
end
local control = false
while not closeNow do
local e = {coroutine.yield()}
if e[1] == "k.procdie" then
@ -165,32 +341,16 @@ while not closeNow do
key(c, 0)
end
end
line(sH)
draw(#console + 1)
elseif e[3] == "key" then
if e[5] == 29 or e[5] == 157 then
control = e[6]
elseif e[6] then
if not control then
key(e[4] ~= 0 and unicode.char(e[4]), e[5])
line(sH)
elseif e[5] == 203 and sW > 8 then
sW = sW - 1
window.setSize(sW, sH)
elseif e[5] == 200 and sH > 2 then
sH = sH - 1
table.remove(console, 1)
window.setSize(sW, sH)
elseif e[5] == 205 then
sW = sW + 1
window.setSize(sW, sH)
elseif e[5] == 208 then
sH = sH + 1
table.insert(console, 1, "")
window.setSize(sW, sH)
end
key(e[4] ~= 0 and unicode.char(e[4]), e[5])
draw(#console + 1)
end
elseif e[3] == "line" then
line(e[4])
draw(e[4])
end
end
end

View File

@ -616,7 +616,7 @@ local function key(ku, ka, kc, down)
elseif kc == 56 then
isAltDown = down
end
if isAltDown and kc == 122 then
if isAltDown and ka == 122 then
if focus and down then
local n = table.remove(surfaces, 1)
table.insert(surfaces, n)

View File

@ -81,20 +81,12 @@ local function getPfx(xd, pkg)
end
end
local endAcPattern = "/[a-z0-9/%.]*$"
local function matchesSvc(xd, pkg, perm)
local pfx = getPfx(xd, pkg)
if pfx then
local permAct = perm
local paP = permAct:match(endAcPattern)
if paP then
permAct = permAct:sub(1, #permAct - #paP)
end
if permAct == pfx then
return "allow"
end
local function splitAC(ac)
local sb = ac:match("/[a-z0-9/%.]*$")
if sb then
return ac:sub(1, #ac - #sb), sb
end
return ac
end
donkonitDFProvider(function (pkg, pid, sendSig)
@ -130,7 +122,8 @@ donkonitDFProvider(function (pkg, pid, sendSig)
myApi = getPfx("", pkg),
lockPerm = function (perm)
-- Are we allowed to?
if not matchesSvc("x.", pkg, perm) then
local permPfx, detail = splitAC(perm)
if getPfx("x.", pkg) ~= permPfx then
return false, "You don't own this permission."
end
local set = "perm|*|" .. perm
@ -223,7 +216,11 @@ local function secPolicyStage2(pid, proc, perm, req)
-- Push to ICECAP thread to avoid deadlock b/c wrong event-pull context
neo.scheduleTimer(0)
table.insert(todo, function ()
local ok, err = pcall(secpol, nexus, settings, proc.pkg, pid, perm, req, matchesSvc)
local fPerm = perm
if fPerm:sub(1, 2) == "r." then
fPerm = splitAC(fPerm)
end
local ok, err = pcall(secpol, nexus, settings, proc.pkg, pid, fPerm, req, getPfx("", proc.pkg))
if not ok then
neo.emergency("Used fallback policy because of run-err: " .. err)
req(def)
@ -243,11 +240,7 @@ rootAccess.securityPolicy = function (pid, proc, perm, req)
end
-- Do we need to start it?
if perm:sub(1, 6) == "x.svc." and not neo.usAccessExists(perm) then
local appAct = perm:sub(7)
local paP = appAct:match(endAcPattern)
if paP then
appAct = appAct:sub(1, #appAct - #paP)
end
local appAct = splitAC(perm:sub(3))
-- Prepare for success
onReg[perm] = onReg[perm] or {}
local orp = onReg[perm]

View File

@ -459,7 +459,7 @@ function retrieveAccess(perm, pkg, pid)
accesses[uid] = function (pkg, pid)
return nil
end
return function (f)
return function (f, secret)
-- Registration function
ensureType(f, "function")
local accessObjectCache = {}
@ -481,8 +481,10 @@ function retrieveAccess(perm, pkg, pid)
end
-- returns nil and fails
end
-- Announce registration
distEvent(nil, "k.registration", uid)
if not secret then
-- Announce registration
distEvent(nil, "k.registration", uid)
end
end, function ()
-- Registration becomes null (access is held but other processes cannot retrieve object)
if accesses[uid] then

View File

@ -11,13 +11,19 @@
-- IRC is usually pretty safe, but no guarantees.
-- Returns "allow", "deny", or "ask".
local function actualPolicy(pkg, pid, perm, matchesSvc)
local function actualPolicy(pkg, pid, perm, pkgSvcPfx)
-- System stuff is allowed.
if pkg:sub(1, 4) == "sys-" then
return "allow"
end
-- svc-t's job is solely to emulate terminals
-- TO INSTALL YOUR OWN TERMINAL EMULATOR:
-- perm|app-yourterm|r.neo.t
if pkg == "svc-t" and perm == "r.neo.pub.t" then
return "allow"
end
-- <The following is for apps & services>
-- x.neo.pub (aka Icecap) is open to all
-- x.neo.pub.* is open to all
if perm:sub(1, 10) == "x.neo.pub." then
return "allow"
end
@ -25,7 +31,8 @@ local function actualPolicy(pkg, pid, perm, matchesSvc)
if perm == "s.h.component_added" or perm == "s.h.component_removed" or perm == "s.h.tablet_use" or perm == "c.tablet" then
return "allow"
end
if matchesSvc("r.", pkg, perm) then
-- Userlevel can register for itself
if perm == "r." .. pkgSvcPfx then
return "allow"
end
-- Userlevel has no other registration rights
@ -44,8 +51,8 @@ local function actualPolicy(pkg, pid, perm, matchesSvc)
return "ask"
end
return function (nexus, settings, pkg, pid, perm, rsp, matchesSvc)
local res = actualPolicy(pkg, pid, perm, matchesSvc)
return function (nexus, settings, pkg, pid, perm, rsp, pkgSvcPfx)
local res = actualPolicy(pkg, pid, perm, pkgSvcPfx)
if settings then
res = settings.getSetting("perm|" .. pkg .. "|" .. perm) or
settings.getSetting("perm|*|" .. perm) or res

View File

@ -63,7 +63,8 @@ The security check may be aliased to
"r.*": Registers a service's API for
retrieval via the "x." mechanism.
Returns a:
function (function (pkg, pid, send))
function (function (pkg, pid, send),
secret)
While the registration is locked on
success, attempting to use it will
fail, as no handler has been given.
@ -71,6 +72,11 @@ The security check may be aliased to
registration with a callback used
for when a process tries to use the
registered API.
Unless 'secret' is truthy, a
k.registration event is sent to all
processes; using the secret flag is
useful for a more ad-hoc security
approach.
What that API returns goes to the
target process.
The given "sendSig" function can be

View File

@ -5,20 +5,34 @@ The "svc-t" program / "x.svc.t"
--- THEORETICAL TERMINALS MODEL ---
The theoretical model for terminals
in KittenOS NEO is that of a stack
of processes controlling a player's
connection to a MUD, where text is
provided to the server and to the
player in a line-by-line format,
with no "flow control"/ttyattrs.
in KittenOS NEO is a TELNET client
that only supports the ECHO option,
and uses the non-standard behavior
of treating ECHO ON as 'enable local
line editing'.
To prevent code size going too far,
the client is extremely restricted
in capabilities.
If you really want full support,
write a better terminal application.
Features that get added will be added
in accordance with ANSI/TELNET where
reasonable or in a compatible-ish
fashion where unreasonable.
The defaults will be set based on
whatever app-luashell requires, as
this is what is expected of modern
terminal systems regardless of what
the standards may have to say.
A process starting another process
connected to the same terminal is
advised to wait for that process to
die before continuing in terminal
activities, unless some sort of
'in-band notification' functionality
is intended.
die before continuing reading input.
The controlling process is whichever
process is supposed to be accepting
@ -32,8 +46,8 @@ The controlling process should show
acknowledgement that user input has
been received.
User input IS NOT automatically
echoed by the terminal.
For convenience, terminal echo is on
by default; this is easily remedied.
--- ACTUAL USAGE OF TERMINALS ---
@ -42,12 +56,15 @@ Access control on terminals is looser
to be able to be 'sublet' in some
cases, including events.
As such, the secret flag is set for
terminal registration.
A terminal program is given a string
argument for the ID of the terminal
to connect to.
A terminal always has an ID beginning
with "x.svc.t/". ALWAYS CHECK THIS.
with "x.neo.pub.t/". ALWAYS CHECK.
Requiring the responsible access
connects to the terminal. All
@ -80,11 +97,22 @@ In either case, when the access has
id = "x.svc.t/<...>"
pid = <The terminal's PID.>
line = function (text): Shows a line.
write = function (text): Writes the
TELNET data to the terminal.
When the user sends a line, an event
of: <id>, "line", <text>
is provided.
User input is provided in events:
<id>, "data", <data>
TELNET commands are provided in:
<id>, "telnet", <data>
There is a total of one TELNET
command per event, unpadded.
Notably, intermixing the data part
of the data/telnet events in order
produces the full terminal-to-server
TELNET stream.
-- This is released into
the public domain.