-- cwb.lua -- the live application directory service for the CWB browser.
--
-- This is an HTTP server, not an HTML generator. Installed applications are
-- clients of this service: they discover this instance, connect over ces.conn,
-- and renew a bounded in-memory lease. Browser users connect to GET / and
-- receive a page rendered from the live lease table at that instant.
CES_MANIFEST = { name = "cwb application server", version = "0.2",
description = "live directory of installed CWB applications" }
local LEASE_S = 30 * 60
local REQUEST_MAX = 16 * 1024
local SOURCE = "/s/cwb.lua"
local apps = {}
local function esc(s)
s = tostring(s or "")
return (s:gsub("&", "&"):gsub("<", "<"):gsub(">", ">")
:gsub('"', """):gsub("'", "'"))
end
local function now_s() return math.floor(ces.now() / 1000000) end
local function server_endpoint()
local info = ces.server_info and ces.server_info() or nil
if type(info) ~= "table" then return nil, nil end
local host = tostring(info.server_name or ""):match("^([^:]+)")
if not host or host == "" then return nil, nil end
return host, tonumber(info.rpc_port) or 0
end
local function http(body, status)
body = body or ""
status = status or 200
local reason = status == 404 and "Not Found" or
status == 400 and "Bad Request" or "OK"
return ("HTTP/1.1 %d %s\r\nContent-Type: text/html; charset=utf-8\r\n" ..
"Content-Length: %d\r\nConnection: close\r\n\r\n%s")
:format(status, reason, #body, body)
end
local function parse_request(raw)
local head = raw:match("^(.-)\r\n\r\n")
if not head then return nil end
local first = head:match("^(.-)\r\n") or head
local method, target = first:match("^(%S+)%s+(%S+)")
if not method then return nil end
return {method=method, path=(target:match("^([^?]*)") or "/")}
end
local function live_apps()
local t, out = now_s(), {}
for name, app in pairs(apps) do
if app.expires > t then
out[#out + 1] = app
else
apps[name] = nil
end
end
table.sort(out, function(a, b) return a.name < b.name end)
return out
end
local function page_html()
local host = server_endpoint()
local list = live_apps()
local b = {[[
THIS SERVER
Applications
installed and running here
]]}
if #list == 0 then
b[#b + 1] = "
No applications are currently registered.
\n"
else
for _, app in ipairs(list) do
local url = host and ("luarpc://%s:%d/"):format(host, app.rpc_port) or nil
if url then
b[#b + 1] = ("
%s
%s
\n")
:format(esc(url), esc(app.name), esc(app.description))
else
b[#b + 1] = ("
%s
%s
\n")
:format(esc(app.name), esc(app.description))
end
end
end
b[#b + 1] = "
cwb application server
"
return table.concat(b)
end
local function same_instance(source, rpc_port, pubkey)
local info = ces.server_info and ces.server_info() or nil
if type(info) ~= "table" then return false end
local rpc = tonumber(info.rpc_port) or 0
if rpc == 0 then return false end
-- Validation is service-to-service traffic on this CES host. Using the
-- advertised public name here incorrectly depends on NAT hairpinning.
local cc = ces.compute_client("127.0.0.1:" .. rpc)
if not cc then return false end
local rows = cc:instances(source) or {}
cc:close()
for _, row in ipairs(rows) do
if tonumber(row.rpc_port) == rpc_port and row.program_pubkey == pubkey then
return true
end
end
local function hx(s)
return (tostring(s or ""):gsub(".", function(c)
return string.format("%02x", string.byte(c))
end))
end
ces.log(("cwb: registration mismatch source=%s port=%s caller=%s rows=%d")
:format(source, tostring(rpc_port), hx(pubkey):sub(1, 16), #rows))
for _, row in ipairs(rows) do
ces.log(("cwb: candidate port=%s program=%s")
:format(tostring(row.rpc_port), hx(row.program_pubkey):sub(1, 16)))
end
return false
end
local function register(line, pubkey)
local name, source, description, port =
line:match("^REGISTER|([^|]+)|([^|]+)|([^|]*)|(%d+)$")
port = tonumber(port)
if not name or #name > 64 or #description > 240 or not port or port < 1 or port > 65535 then
return "ERR bad registration"
end
if source:sub(1, 3) ~= "/s/" then return "ERR source" end
if not pubkey or #pubkey ~= 32 then return "ERR identity" end
if not same_instance(source, port, pubkey) then return "ERR not live instance" end
apps[name] = {name=name, source=source, description=description,
rpc_port=port, program_pubkey=pubkey,
expires=now_s() + LEASE_S}
ces.log("cwb: registered " .. name .. " from " .. source)
return "OK " .. tostring(LEASE_S)
end
ces.conn.set_listener({
on_open = function(conn) conn.buf = "" end,
on_data = function(conn, data)
conn.buf = (conn.buf or "") .. data
if #conn.buf > REQUEST_MAX then
conn:write("ERR request too large\n"); conn:close(); return
end
if conn.buf:sub(1, 9) == "REGISTER|" then
local line = conn.buf:match("^([^\n]*)\n")
if line then
local pubkey = conn.pubkey
-- Native client calls yield. Connection callbacks are not coroutines,
-- so validation must run in a behavior rather than inside on_data.
ces.spawn(function()
local reply = register(line, pubkey)
pcall(function() conn:write(reply .. "\n"); conn:close() end)
end)
end
return
end
if conn.buf:find("\r\n\r\n", 1, true) then
local req = parse_request(conn.buf)
if not req or req.method ~= "GET" then
conn:write(http("Bad request
", 400))
elseif req.path == "/" or req.path == "/index.html" then
conn:write(http(page_html()))
else
conn:write(http("Not found
", 404))
end
conn:close()
end
end,
on_close = function() end,
})
-- Remove artifacts produced by the superseded file-as-IPC implementation.
-- The directory is this running program; it never materializes an HTML cache.
pcall(function() ces.file_delete("/s/cwb/registry.log") end)
pcall(function() ces.file_delete("/s/cwb/index.html") end)
ces.log("cwb: live directory service ready (" .. SOURCE .. ")")
ces.run()