From 7c670d9c5c92f7e229b6895625423702d39d16d2 Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Sun, 15 Mar 2020 19:35:46 -0600 Subject: [PATCH] feat: separate registerAPIHandler from registerURLHandler Also, make WebSockets a proper superset of the POST handlers. --- .../lib/ag-solo/register-http.js | 18 ----- .../lib/ag-solo/vats/bootstrap.js | 8 +- .../cosmic-swingset/lib/ag-solo/vats/repl.js | 13 +--- .../lib/ag-solo/vats/vat-http.js | 44 +++++++---- packages/cosmic-swingset/lib/ag-solo/web.js | 78 ++++++++++++------- 5 files changed, 87 insertions(+), 74 deletions(-) delete mode 100644 packages/cosmic-swingset/lib/ag-solo/register-http.js diff --git a/packages/cosmic-swingset/lib/ag-solo/register-http.js b/packages/cosmic-swingset/lib/ag-solo/register-http.js deleted file mode 100644 index 507b27511bd..00000000000 --- a/packages/cosmic-swingset/lib/ag-solo/register-http.js +++ /dev/null @@ -1,18 +0,0 @@ -export default async function registerHttp({ home, bundle }) { - console.error(`Upgrading Dapp handlers...`); - await register(home, bundle, Object.keys(bundle).filter(k => k !== 'main').sort()); -} - -export async function register(homeP, bundle, keys) { - const targetObj = await homeP~.http; - if (!targetObj) { - throw Error(`HTTP registration object not available`); - } - await Promise.all(keys.map(key => { - const { source, moduleFormat } = bundle[key]; - // console.error(`Uploading ${source}`); - - // Register the HTTP handler. - contractsAP.push(targetObj~.register(key, source, moduleFormat)); - })); -} diff --git a/packages/cosmic-swingset/lib/ag-solo/vats/bootstrap.js b/packages/cosmic-swingset/lib/ag-solo/vats/bootstrap.js index 0448772f192..c378be14fee 100644 --- a/packages/cosmic-swingset/lib/ag-solo/vats/bootstrap.js +++ b/packages/cosmic-swingset/lib/ag-solo/vats/bootstrap.js @@ -51,7 +51,7 @@ export default function setup(syscall, state, helpers) { } async function setupWalletVat(commandDevice, httpVat, walletVat) { - await E(httpVat).registerCommandHandler(walletVat); + await E(httpVat).registerURLHandler(walletVat, '/vat'); await E(walletVat).setCommandDevice(commandDevice); await E(walletVat).setPresences(); } @@ -154,10 +154,10 @@ export default function setup(syscall, state, helpers) { await E(wallet).deposit(pursePetnames.moola, moolaPayment); await E(wallet).deposit(pursePetnames.simolean, simoleanPayment); - // This will allow Dapp developers to register in their dapp.js + // This will allow dApp developers to register in their api/deploy.js const httpRegCallback = { - registerCommandHandler(handler) { - return E(vats.http).registerCommandHandler(handler); + registerAPIHandler(handler) { + return E(vats.http).registerURLHandler(handler, '/api'); }, }; diff --git a/packages/cosmic-swingset/lib/ag-solo/vats/repl.js b/packages/cosmic-swingset/lib/ag-solo/vats/repl.js index b792261560c..0b50e926963 100644 --- a/packages/cosmic-swingset/lib/ag-solo/vats/repl.js +++ b/packages/cosmic-swingset/lib/ag-solo/vats/repl.js @@ -26,7 +26,7 @@ export function stringify(value, spaces, already = new WeakSet()) { if (Array.isArray(value)) { let ret = '['; let sep = ''; - for (let i = 0; i < value.length; i++) { + for (let i = 0; i < value.length; i += 1) { ret += sep + stringify(value[i], spaces, already); sep = ','; } @@ -57,7 +57,7 @@ export function getReplHandler(E, homeObjects, sendBroadcast) { const display = {}; let highestHistory = -1; - function updateHistorySlot(histnum, s) { + function updateHistorySlot(histnum) { // console.log(`sendBroadcast ${histnum}`); sendBroadcast({ type: 'updateHistory', @@ -67,13 +67,6 @@ export function getReplHandler(E, homeObjects, sendBroadcast) { }); } - function addException(histnum, e) { - // We stringify so that exceptions do not leak. - const s = `${e}`; - display[histnum] = `Promise.reject(${stringify(s)})`; - updateHistorySlot(histnum); - } - const { evaluateProgram } = makeEvaluators({ sloppyGlobals: true }); return { @@ -83,7 +76,7 @@ export function getReplHandler(E, homeObjects, sendBroadcast) { rebroadcastHistory() { // console.log(`rebroadcastHistory`, highestHistory); - for (let histnum = 0; histnum <= highestHistory; histnum++) { + for (let histnum = 0; histnum <= highestHistory; histnum += 1) { updateHistorySlot(histnum); } }, diff --git a/packages/cosmic-swingset/lib/ag-solo/vats/vat-http.js b/packages/cosmic-swingset/lib/ag-solo/vats/vat-http.js index 395b1b93082..5d1df597f1a 100644 --- a/packages/cosmic-swingset/lib/ag-solo/vats/vat-http.js +++ b/packages/cosmic-swingset/lib/ag-solo/vats/vat-http.js @@ -37,13 +37,18 @@ function build(E, D) { }); harden(readyForClient); - let handler = {}; - const registeredHandlers = []; + const handler = {}; + const registeredURLHandlers = new Map(); // TODO: Don't leak memory. - async function registerCommandHandler(newHandler) { + async function registerURLHandler(newHandler, url) { const commandHandler = await E(newHandler).getCommandHandler(); - registeredHandlers.push(commandHandler); + let reg = registeredURLHandlers.get(url); + if (!reg) { + reg = []; + registeredURLHandlers.set(url, reg); + } + reg.push(commandHandler); } return { @@ -133,7 +138,8 @@ function build(E, D) { } }, - registerCommandHandler, + registerURLHandler, + registerAPIHandler: h => registerURLHandler(h, '/api'), setProvisioner(p) { provisioner = p; @@ -160,30 +166,36 @@ function build(E, D) { // JSON.stringify(obj, undefined, 2), // ); - const { type } = obj; - // Try on local handlers first. - let res; - // Try on registered handlers. - if (type in handler) { - res = await handler[type](obj); - } else { + const { type, requestContext: { url } = { url: '/vat' } } = obj; + if (url === '/vat' || url === '/captp') { + // Use our local handler object (compatibility with repl.js). + // TODO: standardise + if (handler[type]) { + D(commandDevice).sendResponse(count, false, handler[type](obj)); + return; + } + } + + const urlHandlers = registeredURLHandlers.get(url); + if (urlHandlers) { // todo fixme avoid the loop // For now, go from the end to beginning so that handlers // override. const hardObjects = harden({ ...homeObjects }); - for (let i = registeredHandlers.length - 1; i >= 0; i -= 1) { + for (let i = urlHandlers.length - 1; i >= 0; i -= 1) { // eslint-disable-next-line no-await-in-loop - res = await E(registeredHandlers[i]).processInbound( + const res = await E(urlHandlers[i]).processInbound( obj, hardObjects, ); if (res) { - break; + D(commandDevice).sendResponse(count, false, harden(res)); + return; } } } - D(commandDevice).sendResponse(count, false, harden(res)); + throw Error(`No handler for ${url} ${type}`); } catch (rej) { D(commandDevice).sendResponse(count, true, harden(rej)); } diff --git a/packages/cosmic-swingset/lib/ag-solo/web.js b/packages/cosmic-swingset/lib/ag-solo/web.js index a93ccefd942..53f233e5535 100644 --- a/packages/cosmic-swingset/lib/ag-solo/web.js +++ b/packages/cosmic-swingset/lib/ag-solo/web.js @@ -21,9 +21,22 @@ const send = (ws, msg) => { export function makeHTTPListener(basedir, port, host, rawInboundCommand) { // Enrich the inbound command with some metadata. - const inboundCommand = (body, { headers: { origin } = {} } = {}) => { - const requestContext = { origin, date: Date.now() }; - return rawInboundCommand({ ...body, requestContext }); + const inboundCommand = ( + body, + { url, headers: { origin } = {} } = {}, + id = undefined, + ) => { + const obj = { ...body, requestContext: { origin, url, date: Date.now() } }; + return rawInboundCommand(obj).catch(err => { + const idpfx = id ? `${id} ` : ''; + console.error( + `${idpfx}inbound error:`, + err, + 'from', + JSON.stringify(obj, undefined, 2), + ); + throw (err && err.message) || JSON.stringify(err); + }); }; const app = express(); @@ -80,19 +93,24 @@ export function makeHTTPListener(basedir, port, host, rawInboundCommand) { return true; }; - // accept vat messages on /vat - app.post('/vat', (req, res) => { - if (!validateOrigin(req)) { - res.json({ ok: false, rej: 'Invalid Origin' }); - return; - } + // accept messages on /vat and /api + // todo: later allow arbitrary endpoints? + for (const ep of ['/vat', '/api']) { + app.post(ep, (req, res) => { + if (ep !== '/api' && !validateOrigin(req)) { + res.json({ ok: false, rej: 'Invalid Origin' }); + return; + } - // console.log(`POST /vat got`, req.body); // should be jsonable - inboundCommand(req.body, req).then( - r => res.json({ ok: true, res: r }), - rej => res.json({ ok: false, rej }), - ); - }); + // console.log(`POST /vat got`, req.body); // should be jsonable + inboundCommand(req.body, req) + .then( + r => res.json({ ok: true, res: r }), + rej => res.json({ ok: false, rej }), + ) + .catch(_ => {}); + }); + } // accept WebSocket connections at the root path. // This senses the Connection-Upgrade header to distinguish between plain @@ -132,7 +150,11 @@ export function makeHTTPListener(basedir, port, host, rawInboundCommand) { console.log(id, 'client closed'); broadcasts.delete(ws); if (req.url === '/captp') { - inboundCommand({ type: 'CTP_CLOSE', connectionID }, req).catch(_ => {}); + inboundCommand( + { type: 'CTP_CLOSE', connectionID }, + req, + id, + ).catch(_ => {}); points.delete(connectionID); } }); @@ -140,18 +162,18 @@ export function makeHTTPListener(basedir, port, host, rawInboundCommand) { if (req.url === '/captp') { // This is a point-to-point connection, not broadcast. points.set(connectionID, ws); - inboundCommand({ type: 'CTP_OPEN', connectionID }, req).catch(err => { - console.log(id, `error establishing connection`, err); - }); + inboundCommand( + { type: 'CTP_OPEN', connectionID }, + req, + id, + ).catch(_ => {}); ws.on('message', async message => { try { // some things use inbound messages const obj = JSON.parse(message); obj.connectionID = connectionID; - await inboundCommand(obj, req); - } catch (e) { - console.log(id, 'client error', e); - const error = e.message || JSON.stringify(e); + await inboundCommand(obj, req, id); + } catch (error) { // eslint-disable-next-line no-use-before-define sendJSON({ type: 'CTP_ERROR', connectionID, error }); } @@ -159,9 +181,13 @@ export function makeHTTPListener(basedir, port, host, rawInboundCommand) { } else { broadcasts.set(connectionID, ws); ws.on('message', async message => { - // we ignore messages arriving on the default websocket port, it is only for - // outbound broadcasts - console.log(id, `ignoring message on WS port`, String(message)); + try { + const obj = JSON.parse(message); + // eslint-disable-next-line no-use-before-define + sendJSON(await inboundCommand(obj, req, id)); + } catch (error) { + // ignore + } }); } }