Skip to content

Commit

Permalink
fix: change webkey -> accessToken and polish usage
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelfig committed Sep 13, 2020
1 parent b2a5319 commit 0362abe
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 106 deletions.
23 changes: 8 additions & 15 deletions packages/agoric-cli/lib/deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { makePromiseKit } from '@agoric/promise-kit';
import bundleSource from '@agoric/bundle-source';
import path from 'path';

import { getWebkey } from './open';
import { getAccessToken } from './open';

// note: CapTP has its own HandledPromise instantiation, and the contract
// must use the same one that CapTP uses. We achieve this by not bundling
Expand Down Expand Up @@ -60,21 +60,14 @@ export default async function deployMain(progname, rawArgs, powers, opts) {
() => process.stdout.write(progressDot),
1000,
);

const retryWebsocket = async () => {
let wskeyurl;
try {
const webkey = await getWebkey(opts.hostport);
wskeyurl = `${wsurl}?webkey=${encodeURIComponent(webkey)}`;
} catch (e) {
if (e.code === 'ECONNREFUSED' && !connected) {
// Retry in a little bit.
setTimeout(retryWebsocket, RETRY_DELAY_MS);
} else {
console.error(`Trying to fetch webkey:`, e);
}
return;
}
const ws = makeWebSocket(wskeyurl, { origin: 'http://127.0.0.1' });
const accessToken = await getAccessToken(opts.hostport);

// For a WebSocket we need to put the token in the query string.
const wsWebkey = `${wsurl}?accessToken=${encodeURIComponent(accessToken)}`;

const ws = makeWebSocket(wsWebkey, { origin: 'http://127.0.0.1' });
ws.on('open', async () => {
connected = true;
try {
Expand Down
10 changes: 8 additions & 2 deletions packages/agoric-cli/lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,15 @@ const main = async (progname, rawArgs, powers) => {
'127.0.0.1:8000',
)
.option(
'--repl <both | only | none>',
'--repl <yes | only | no>',
'whether to show the Read-eval-print loop',
'none',
value => {
if (['yes', 'only', 'no'].includes(value)) {
return value;
}
throw TypeError(`--repl must be one of 'yes', 'no', or 'only'`);
},
'no',
)
.action(async cmd => {
const opts = { ...program.opts(), ...cmd.opts() };
Expand Down
90 changes: 50 additions & 40 deletions packages/agoric-cli/lib/open.js
Original file line number Diff line number Diff line change
@@ -1,48 +1,69 @@
import http from 'http';
import fs from 'fs';
/* global globalThis */
import defaultHttp from 'http';
import { promises as defaultFs } from 'fs';
import opener from 'opener';

const RETRY_DELAY_MS = 1000;

export async function getWebkey(hostport) {
const basedir = await new Promise((resolve, reject) => {
const req = http.get(`http://${hostport}/ag-solo-basedir`, res => {
let buf = '';
res.on('data', chunk => {
buf += chunk;
export async function getAccessToken(hostport, powers = {}) {
const { fs = defaultFs, http = defaultHttp } = powers;

if (!hostport.match(/^(localhost|127\.0\.0\.1):\d+$/)) {
throw TypeError(`Only localhost is supported, not ${hostport}`);
}

const lookupToken = async () => {
const basedir = await new Promise((resolve, reject) => {
const req = http.get(`http://${hostport}/ag-solo-basedir`, res => {
let buf = '';
res.on('data', chunk => {
buf += chunk;
});
res.on('close', () => resolve(buf));
});
req.on('error', e => {
reject(e);
});
res.on('close', () => resolve(buf));
});
req.on('error', e => {
reject(e);
});
});

const privateWebkey = fs.readFileSync(
`${basedir}/private-webkey.txt`,
'utf-8',
);
return fs.readFile(`${basedir}/private-access-token.txt`, 'utf-8');
};

return privateWebkey;
return new Promise((resolve, reject) => {
const retryGetAccessToken = async () => {
try {
const accessToken = await lookupToken();
resolve(accessToken);
} catch (e) {
if (e.code === 'ECONNREFUSED') {
// Retry in a little bit.
setTimeout(retryGetAccessToken, RETRY_DELAY_MS);
} else {
reject(e);
}
}
};
retryGetAccessToken();
});
}

export default async function walletMain(progname, rawArgs, powers, opts) {
const { anylogger } = powers;
const { anylogger, fs, http } = powers;
const console = anylogger('agoric:wallet');

let suffix;
switch (opts.repl) {
case 'both':
case 'yes':
suffix = '';
break;
case 'none':
case 'no':
suffix = '/wallet';
break;
case 'only':
suffix = '?w=0';
break;
default:
throw Error(`--repl must be one of 'both', 'none', or 'only'`);
throw TypeError(`Unexpected --repl option ${JSON.stringify(opts.repl)}`);
}

process.stderr.write(`Launching wallet...`);
Expand All @@ -51,31 +72,20 @@ export default async function walletMain(progname, rawArgs, powers, opts) {
() => process.stderr.write(progressDot),
1000,
);
const walletWebkey = await new Promise((resolve, reject) => {
const retryGetWebkey = async () => {
try {
const webkey = await getWebkey(opts.hostport);
resolve(webkey);
} catch (e) {
if (e.code === 'ECONNREFUSED') {
// Retry in a little bit.
setTimeout(retryGetWebkey, RETRY_DELAY_MS);
} else {
console.error(`Trying to fetch webkey:`, e);
reject(e);
}
}
};
retryGetWebkey();
});

const walletAccessToken = await getAccessToken(opts.hostport, {
console,
fs,
http,
}).catch(e => console.error(`Trying to fetch access token:`, e));

clearInterval(progressTimer);
process.stderr.write('\n');

// Write out the URL and launch the web browser.
const walletUrl = `http://${
opts.hostport
}${suffix}#webkey=${encodeURIComponent(walletWebkey)}`;
}${suffix}#accessToken=${encodeURIComponent(walletAccessToken)}`;

process.stdout.write(`${walletUrl}\n`);
const browser = opener(walletUrl);
Expand Down
52 changes: 28 additions & 24 deletions packages/cosmic-swingset/lib/ag-solo/html/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,41 @@ const RECONNECT_BACKOFF_SECONDS = 3;
const resetFns = [];
let inpBackground;

if (!window.location.hash) {
// Clear out the hash for privacy.
const accessTokenParams = `?${window.location.hash.slice(1)}`;
const accessTokenHash = window.location.hash;
window.location.hash = '';
const hasAccessToken = new URLSearchParams(accessTokenParams).has(
'accessToken',
);

if (!hasAccessToken) {
// This is friendly advice to the user who doesn't know.
// eslint-disable-next-line no-alert
window.alert(
`\
if (
// eslint-disable-next-line no-alert
window.confirm(
`\
You must open the Agoric Wallet+REPL with the
agoric open --repl=both
agoric open --repl=yes
command line executable.
`,
);
window.location =
'https://agoric.com/documentation/getting-started/agoric-cli-guide.html#agoric-open';
See the documentation?`,
)
) {
window.location.href =
'https://agoric.com/documentation/getting-started/agoric-cli-guide.html#agoric-open';
}
}

function run() {
const disableFns = []; // Functions to run when the input should be disabled.
resetFns.push(() => (document.querySelector('#history').innerHTML = ''));

const loc = window.location;

const urlParams = `?${loc.hash.slice(1)}`;
// TODO: Maybe clear out the hash for privacy.
// loc.hash = 'webkey=*redacted*';

let nextHistNum = 0;
let inputHistoryNum = 0;

async function call(req) {
const res = await fetch(`/private/repl${urlParams}`, {
const res = await fetch(`/private/repl${accessTokenParams}`, {
method: 'POST',
body: JSON.stringify(req),
headers: { 'Content-Type': 'application/json' },
Expand All @@ -44,8 +50,8 @@ function run() {
throw new Error(`server error: ${JSON.stringify(j.rej)}`);
}

const protocol = loc.protocol.replace(/^http/, 'ws');
const socketEndpoint = `${protocol}//${loc.host}/private/repl${urlParams}`;
const protocol = window.location.protocol.replace(/^http/, 'ws');
const socketEndpoint = `${protocol}//${window.location.host}/private/repl${accessTokenParams}`;
const ws = new WebSocket(socketEndpoint);

ws.addEventListener('error', ev => {
Expand Down Expand Up @@ -262,11 +268,9 @@ function run() {
resetFns.push(() =>
document.getElementById('go').removeAttribute('disabled'),
);

return urlParams;
}

const urlParams = run();
run();

// Display version information, if possible.
const fetches = [];
Expand Down Expand Up @@ -296,16 +300,16 @@ fetches.push(fpj);

// an optional `w=0` GET argument will suppress showing the wallet
if (
window.location.hash &&
hasAccessToken &&
new URLSearchParams(window.location.search).get('w') !== '0'
) {
fetch(`wallet/${urlParams}`)
fetch(`wallet/${accessTokenParams}`)
.then(resp => {
if (resp.status < 200 || resp.status >= 300) {
throw Error(`status ${resp.status}`);
}
walletFrame.style.display = 'block';
walletFrame.src = `wallet/${window.location.hash}`;
walletFrame.src = `wallet/${accessTokenHash}`;
})
.catch(e => {
console.log('Cannot fetch wallet/', e);
Expand Down
36 changes: 22 additions & 14 deletions packages/cosmic-swingset/lib/ag-solo/web.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ const send = (ws, msg) => {
};

// From https://stackoverflow.com/a/43866992/14073862
export function generateToken({ stringBase = 'base64', byteLength = 48 } = {}) {
export function generateAccessToken({
stringBase = 'base64',
byteLength = 48,
} = {}) {
return new Promise((resolve, reject) =>
crypto.randomBytes(byteLength, (err, buffer) => {
if (err) {
Expand All @@ -40,10 +43,12 @@ export function generateToken({ stringBase = 'base64', byteLength = 48 } = {}) {
export async function makeHTTPListener(basedir, port, host, rawInboundCommand) {
// Ensure we're protected with a unique webkey for this basedir.
fs.chmodSync(basedir, 0o700);
const privateWebkeyFile = path.join(basedir, 'private-webkey.txt');
if (!fs.existsSync(privateWebkeyFile)) {
const privateAccessTokenFile = path.join(basedir, 'private-access-token.txt');
if (!fs.existsSync(privateAccessTokenFile)) {
// Create the unique string for this basedir.
fs.writeFileSync(privateWebkeyFile, await generateToken(), { mode: 0o600 });
fs.writeFileSync(privateAccessTokenFile, await generateAccessToken(), {
mode: 0o600,
});
}

// Enrich the inbound command with some metadata.
Expand All @@ -52,7 +57,8 @@ export async function makeHTTPListener(basedir, port, host, rawInboundCommand) {
{ channelID, dispatcher, url, headers: { origin } = {} } = {},
id = undefined,
) => {
// Strip away the query params, as the webkey is there.
// Strip away the query params, as the inbound command device can't handle
// it and the accessToken is there.
const qmark = url.indexOf('?');
const shortUrl = qmark < 0 ? url : url.slice(0, qmark);
const obj = {
Expand Down Expand Up @@ -100,7 +106,7 @@ export async function makeHTTPListener(basedir, port, host, rawInboundCommand) {
log(`Serving static files from ${htmldir}`);
app.use(express.static(htmldir));

const validateOriginAndWebkey = req => {
const validateOriginAndAccessToken = req => {
const { origin } = req.headers;
const id = `${req.socket.remoteAddress}:${req.socket.remotePort}:`;

Expand All @@ -110,15 +116,16 @@ export async function makeHTTPListener(basedir, port, host, rawInboundCommand) {
}

// Validate the private webkey.
const privateWebkey = fs.readFileSync(privateWebkeyFile, 'utf-8');
const reqWebkey = new URL(`http://localhost${req.url}`).searchParams.get(
'webkey',
const accessToken = fs.readFileSync(privateAccessTokenFile, 'utf-8');
const reqToken = new URL(`http://localhost${req.url}`).searchParams.get(
'accessToken',
);
if (reqWebkey !== privateWebkey) {

if (reqToken !== accessToken) {
log.error(
id,
`Invalid webkey ${JSON.stringify(
reqWebkey,
`Invalid access token ${JSON.stringify(
reqToken,
)}; try running "agoric open"`,
);
return false;
Expand All @@ -134,6 +141,7 @@ export async function makeHTTPListener(basedir, port, host, rawInboundCommand) {

if (['chrome-extension:', 'moz-extension:'].includes(url.protocol)) {
// Extensions such as metamask are local and can access the wallet.
// Especially since the access token has been supplied.
return true;
}

Expand All @@ -158,7 +166,7 @@ export async function makeHTTPListener(basedir, port, host, rawInboundCommand) {

// accept POST messages to arbitrary endpoints
app.post('*', (req, res) => {
if (!validateOriginAndWebkey(req)) {
if (!validateOriginAndAccessToken(req)) {
res.json({ ok: false, rej: 'Unauthorized' });
return;
}
Expand All @@ -177,7 +185,7 @@ export async function makeHTTPListener(basedir, port, host, rawInboundCommand) {
// GETs (which should return index.html) and WebSocket requests.
const wss = new WebSocket.Server({ noServer: true });
server.on('upgrade', (req, socket, head) => {
if (!validateOriginAndWebkey(req)) {
if (!validateOriginAndAccessToken(req)) {
socket.destroy();
return;
}
Expand Down
Loading

0 comments on commit 0362abe

Please sign in to comment.