From 85737b48989f95f51908e52415b10abc9bb0aef6 Mon Sep 17 00:00:00 2001 From: Andre Staltz Date: Tue, 16 Jan 2024 13:58:32 +0200 Subject: [PATCH] update to hubClient getting favorite hubs --- lib/index.js | 241 +++++++++++++++++++++-------------- package.json | 3 +- test/createForFriend.test.js | 12 +- test/createForMyself.test.js | 12 +- test/parse.test.js | 8 +- 5 files changed, 166 insertions(+), 110 deletions(-) diff --git a/lib/index.js b/lib/index.js index 6b2a846..c2d4e2f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,16 +1,16 @@ // @ts-ignore -const MultiserverAddress = require('multiserver-address') -// @ts-ignore const ip = require('ip') const p = require('promisify-tuple') /** * @typedef {{ pubkey: string }} SHSE * @typedef {ReturnType} PPPPPPromise - * @typedef {{connect: (addr: string, cb: CB) => void}} PPPPPNet + * @typedef {ReturnType} PPPPPNet + * @typedef {ReturnType} PPPPPHubClient + * @typedef {import('ppppp-hub-client/plugin').HubMultiaddr} HubMultiaddr * @typedef {{ * type: 'join', - * address: string, + * multiaddr: string, * }} JoinCommand * @typedef {`join/${string}/${string}/${string}/${string}/${string}/${string}`} JoinCommandStr * @typedef {{ @@ -20,7 +20,7 @@ const p = require('promisify-tuple') * @typedef {`follow/${string}`} FollowCommandStr * @typedef {{ * type: 'tunnel-connect', - * address: string, + * multiaddr: string, * }} TunnelConnectCommand * @typedef {`tunnel-connect/${string}/${string}`} TunnelConnectCommandStr * @typedef {{ @@ -41,6 +41,12 @@ const p = require('promisify-tuple') * | PromiseFollowCommand * | PromiseAccountAddCommand * } Command + * @typedef {{ + * shse: SHSE; + * promise: PPPPPPromise; + * hubClient: PPPPPHubClient; + * net: PPPPPNet; + * }} Peer */ /** @@ -96,9 +102,8 @@ function parseJoinCommand(pieces, uri) { pieces.shift() pieces.shift() pieces.shift() - const shse = `shse:${cred.replace('.', ':')}` - const address = `net:${host}:${port}~${shse}` // TODO: add ws address here - return { type: 'join', address } + const multiaddr = `/${hostFormat}/${host}/${transport}/${port}/${transform}/${cred}` + return { type: 'join', multiaddr } } /** @@ -137,8 +142,8 @@ function parseTunnelConnectCommand(pieces, uri) { pieces.shift() pieces.shift() pieces.shift() - const address = `tunnel:${hubPubkey}:${targetPubkey}~shse:${targetPubkey}` - return { type: 'tunnel-connect', address } + const multiaddr = `/tunnel/${hubPubkey}.${targetPubkey}/shse/${targetPubkey}` + return { type: 'tunnel-connect', multiaddr } } /** @@ -240,11 +245,91 @@ function parse(uri) { } /** - * @param {{ - * shse: SHSE; - * promise: PPPPPPromise; - * net: PPPPPNet; - * }} peer + * Among the join commands, find the first valid hub, and return its + * - web protocol (http or https) + * - web hostname + * - pubkey for shse + * @param {Array} joinCommands + * @return {['http' | 'https', string, string]} + */ +function getFirstHub(joinCommands) { + for (const joinCmd of joinCommands) { + const [, hostFormat, host, , , shse, cred] = joinCmd.split('/') + if (shse !== 'shse') continue + const [pubkey, token] = cred.split('.') + if (hostFormat === 'dns') { + return ['https', host, pubkey] + } else { + return ['http', host, pubkey] + } + } + throw new Error('No join commands found') +} + +/** + * + * @param {Peer} peer + * @param {number} amountHubs + * @param {HubMultiaddr=} hardcodedHub + * @returns {Promise<[Error] | [null, Array]>} + */ +async function makeJoinCommands(peer, amountHubs, hardcodedHub) { + /**@type {Array}*/ + const joinCommands = [] + + // Get multiaddr of hubs + const [err1, hubMultiaddrs] = hardcodedHub + ? [null, [hardcodedHub]] + : await p(peer.hubClient.getHubs)(amountHubs) + // prettier-ignore + if (err1) return [new Error('Failed to get hubs while creating invite', { cause: err1 })] + + // For each hub, connect and create token + const hubErrors = [] + for (const multiaddr of hubMultiaddrs) { + const [err2, rpc] = await p(peer.net.connect)(multiaddr) + if (err2) { + // prettier-ignore + hubErrors.push(new Error('Failed to connect to hub while creating invite', { cause: err2 })) + continue + } + + // @ts-ignore + if (!rpc.hub) continue + // @ts-ignore + const [err3, hubToken] = await p(rpc.hub.createToken)() + if (err3) { + // prettier-ignore + hubErrors.push(new Error('Failed to create hub token while creating invite', { cause: err3 })) + continue + } + + if (/shse\/([^.]+)$/.test(multiaddr) === false) { + // prettier-ignore + hubErrors.push(new Error(`Invalid hub multiaddr "${multiaddr}" missing shse portion while creating invite`)) + continue + } + const joinCommand = `join${multiaddr}.${hubToken}` + // @ts-ignore + joinCommands.push(joinCommand) + } + + // If there are no successful join commands, return error + if (joinCommands.length === 0) { + if (hubErrors.length === 0) { + // prettier-ignore + return [new Error('Failed to coordinate with hubs while creating invite, for unknown reasons')] + } + const cause = new AggregateError(hubErrors) + // prettier-ignore + return [new Error('Failed to coordinate with hubs while creating invite', { cause })] + } + + return [null, joinCommands] +} + +/** + * @param {Peer} peer * @param {unknown} config */ function initInvite(peer, config) { @@ -252,7 +337,7 @@ function initInvite(peer, config) { * @param {{ * hubs?: number, * id: string, - * _hubMsAddr?: string, + * _hubMultiaddr?: HubMultiaddr * }} opts * * @param {CB<{uri: string, url: string}>} cb @@ -265,59 +350,37 @@ function initInvite(peer, config) { // prettier-ignore return cb(new Error(`invite.createForFriend opts.id is required for type "follow"`)) } - const hubs = opts.hubs ?? 1 - if (typeof hubs !== 'number') { + const amountHubs = opts.hubs ?? 1 + if (typeof amountHubs !== 'number') { // prettier-ignore - return cb(new Error(`invite.createForFriend opts.hubs should be a number but was ${hubs}`)) + return cb(new Error(`invite.createForFriend opts.hubs should be a number but was ${amountHubs}`)) } - if (!opts._hubMsAddr) { - // prettier-ignore - // FIXME: load hubs from ppppp-net - return cb(new Error(`invite.createForFriend expected opts._hubMsAddr because loading from connDB not yet supported`)) - } - - // Connect to hub and create token - const [err, rpc] = await p(peer.net.connect)(opts._hubMsAddr) - if (err) return cb(err) - const [err2, hubToken] = await p(rpc.hub.createToken)() - if (err2) return cb(err2) - - // Parse multiserver address - // prettier-ignore - const ERROR_MSG = `Invalid multiserver address ${opts._hubMsAddr} for invite.createForFriend` - const msAddr = MultiserverAddress.decode(opts._hubMsAddr) - const [netShsAddr, wsShsAddr] = msAddr - if (!netShsAddr) return cb(new Error(ERROR_MSG)) - const [net, shse] = netShsAddr - if (!net) return cb(new Error(ERROR_MSG)) - if (net.name !== 'net') return cb(new Error(ERROR_MSG)) - const [host, port] = net.data - const hostFormat = ip.isV4Format(host) - ? 'ip4' - : ip.isV6Format('ipv6') - ? 'ip6' - : 'dns' - if (!shse) return cb(new Error(ERROR_MSG)) - if (shse.name !== 'shse') return cb(new Error(ERROR_MSG)) - const [pubkey] = shse.data + // Create "join hub" commands + const [err1, joinCommands] = await makeJoinCommands( + peer, + amountHubs, + opts._hubMultiaddr + ) + if (err1) return cb(err1) + const [protocol, hostname] = getFirstHub(joinCommands) // Create follow promise - const [err3, token] = await p(peer.promise.create)({ + const [err2, token] = await p(peer.promise.create)({ account: opts.id, type: 'follow', }) - if (err3) return cb(err3) - - /** @type {JoinCommandStr} */ - const joinCommand = `join/${hostFormat}/${host}/tcp/${port}/shse/${pubkey}.${hubToken}` - /** @type {FollowCommandStr} */ - const followCommand = `follow/${opts.id}` + if (err2) return cb(err2) /** @type {PromiseFollowCommandStr} */ const promiseCommand = `promise.follow/account.${opts.id}/${token}` - const uri = `ppppp://invite/${joinCommand}/${followCommand}/${promiseCommand}` - const url = `http://${host}/invite#${encodeURIComponent(uri)}` + // Create follow command + /** @type {FollowCommandStr} */ + const followCommand = `follow/${opts.id}` + + // prettier-ignore + const uri = `ppppp://invite/${joinCommands.join('/')}/${followCommand}/${promiseCommand}` + const url = `${protocol}://${hostname}/invite#${encodeURIComponent(uri)}` cb(null, { uri, url }) } @@ -325,7 +388,7 @@ function initInvite(peer, config) { * @param {{ * hubs?: number, * id: string, - * _hubMsAddr?: string, + * _hubMultiaddr?: HubMultiaddr * }} opts * * @param {CB<{uri: string, url: string}>} cb @@ -338,55 +401,35 @@ function initInvite(peer, config) { // prettier-ignore return cb(new Error(`invite.createForMyself opts.id is required for type "follow"`)) } - const hubs = opts.hubs ?? 1 - if (typeof hubs !== 'number') { + const amountHubs = opts.hubs ?? 1 + if (typeof amountHubs !== 'number') { // prettier-ignore - return cb(new Error(`invite.createForMyself opts.hubs should be a number but was ${hubs}`)) + return cb(new Error(`invite.createForMyself opts.hubs should be a number but was ${amountHubs}`)) } - if (!opts._hubMsAddr) { - // prettier-ignore - return cb(new Error(`invite.createForMyself expected opts._hubMsAddr because loading from connDB not yet supported`)) - } - - // Connect to hub and create token - const [err, rpc] = await p(peer.net.connect)(opts._hubMsAddr) - if (err) return cb(err) - const [err2, hubToken] = await p(rpc.hub.createToken)() - if (err2) return cb(err2) - - // Parse multiserver address - // prettier-ignore - const ERROR_MSG = `Invalid multiserver address ${opts._hubMsAddr} for invite.createForMyself` - const msAddr = MultiserverAddress.decode(opts._hubMsAddr) - const [netShsAddr, wsShsAddr] = msAddr - if (!netShsAddr) return cb(new Error(ERROR_MSG)) - const [net, shse] = netShsAddr - if (!net) return cb(new Error(ERROR_MSG)) - if (net.name !== 'net') return cb(new Error(ERROR_MSG)) - const [host, port] = net.data - const hostFormat = ip.isV4Format(host) - ? 'ip4' - : ip.isV6Format('ipv6') - ? 'ip6' - : 'dns' - if (!shse) return cb(new Error(ERROR_MSG)) - if (shse.name !== 'shse') return cb(new Error(ERROR_MSG)) - const [pubkey] = shse.data + // Create "join hub" commands + const [err1, joinCommands] = await makeJoinCommands( + peer, + amountHubs, + opts._hubMultiaddr + ) + if (err1) return cb(err1) + const [protocol, hostname, pubkey] = getFirstHub(joinCommands) // Create account-add promise const promise = { type: 'account-add', account: opts.id } const [err3, token] = await p(peer.promise.create)(promise) if (err3) return cb(err3) - - /** @type {JoinCommandStr} */ - const joinCommand = `join/${hostFormat}/${host}/tcp/${port}/shse/${pubkey}.${hubToken}` - /** @type {TunnelConnectCommandStr} */ - const tunnelCommand = `tunnel-connect/${pubkey}/${peer.shse.pubkey}` /** @type {PromiseAccountAddCommandStr} */ const promiseCommand = `promise.account-add/account.${opts.id}/${token}` - const uri = `ppppp://invite/${joinCommand}/${tunnelCommand}/${promiseCommand}` - const url = `http://${host}/invite#${encodeURIComponent(uri)}` + + // Create tunnel-connect command + /** @type {TunnelConnectCommandStr} */ + const tunnelCommand = `tunnel-connect/${pubkey}/${peer.shse.pubkey}` + + // prettier-ignore + const uri = `ppppp://invite/${joinCommands.join('/')}/${tunnelCommand}/${promiseCommand}` + const url = `${protocol}://${hostname}/invite#${encodeURIComponent(uri)}` cb(null, { uri, url }) } @@ -394,7 +437,7 @@ function initInvite(peer, config) { } exports.name = 'invite' -exports.needs = ['shse', 'promise', 'net'] +exports.needs = ['shse', 'promise', 'net', 'hubClient'] exports.manifest = { createForFriend: 'async', createForMyself: 'async', diff --git a/package.json b/package.json index d50351d..737a14a 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ }, "dependencies": { "ip": "~1.1.8", - "multiserver-address": "~1.0.1", "promisify-tuple": "1.2.0" }, "devDependencies": { @@ -32,7 +31,9 @@ "c8": "^7.11.0", "husky": "^4.3.0", "ppppp-caps": "github:staltz/ppppp-caps", + "ppppp-hub-client": "github:staltz/ppppp-hub-client", "ppppp-keypair": "github:staltz/ppppp-keypair", + "ppppp-net": "github:staltz/ppppp-net", "ppppp-promise": "github:staltz/ppppp-promise", "prettier": "^2.6.2", "pretty-quick": "^3.1.3", diff --git a/test/createForFriend.test.js b/test/createForFriend.test.js index c5bb24d..5227c34 100644 --- a/test/createForFriend.test.js +++ b/test/createForFriend.test.js @@ -25,7 +25,7 @@ test('createForFriend()', async (t) => { return { connect(address, cb) { connectCalled = true - assert.equal(address, 'net:example.com:8008~shse:HUB_PUBKEY') + assert.equal(address, '/dns/example.com/tcp/8008/shse/HUB_PUBKEY') const mockRpc = { hub: { createToken(cb) { @@ -56,11 +56,17 @@ test('createForFriend()', async (t) => { }, } + const mockHubClient = { + name: 'hubClient', + init() {} + } + const local = require('secret-stack/bare')() .use(require('secret-stack/plugins/net')) .use(require('secret-handshake-ext/secret-stack')) .use(mockNet) .use(mockPromise) + .use(mockHubClient) .use(require('../lib')) .call(null, { shse: { @@ -78,7 +84,7 @@ test('createForFriend()', async (t) => { }) const { uri, url } = await p(local.invite.createForFriend)({ - _hubMsAddr: 'net:example.com:8008~shse:HUB_PUBKEY', + _hubMultiaddr: '/dns/example.com/tcp/8008/shse/HUB_PUBKEY', id: 'MOCK_ID', }) assert.equal( @@ -87,7 +93,7 @@ test('createForFriend()', async (t) => { ) assert.equal( url, - `http://example.com/invite#ppppp%3A%2F%2Finvite%2Fjoin%2Fdns%2Fexample.com%2Ftcp%2F8008%2Fshse%2FHUB_PUBKEY.MOCK_TOKEN%2Ffollow%2FMOCK_ID%2Fpromise.follow%2Faccount.MOCK_ID%2FMOCK_PROMISE` + `https://example.com/invite#ppppp%3A%2F%2Finvite%2Fjoin%2Fdns%2Fexample.com%2Ftcp%2F8008%2Fshse%2FHUB_PUBKEY.MOCK_TOKEN%2Ffollow%2FMOCK_ID%2Fpromise.follow%2Faccount.MOCK_ID%2FMOCK_PROMISE` ) assert.ok(connectCalled) diff --git a/test/createForMyself.test.js b/test/createForMyself.test.js index efabe00..2fbff45 100644 --- a/test/createForMyself.test.js +++ b/test/createForMyself.test.js @@ -25,7 +25,7 @@ test('createForMyself()', async (t) => { return { connect(address, cb) { connectCalled = true - assert.equal(address, 'net:example.com:8008~shse:HUB_PUBKEY') + assert.equal(address, '/dns/example.com/tcp/8008/shse/HUB_PUBKEY') const mockRpc = { hub: { createToken(cb) { @@ -56,11 +56,17 @@ test('createForMyself()', async (t) => { }, } + const mockHubClient = { + name: 'hubClient', + init() {}, + } + const local = require('secret-stack/bare')() .use(require('secret-stack/plugins/net')) .use(require('secret-handshake-ext/secret-stack')) .use(mockNet) .use(mockPromise) + .use(mockHubClient) .use(require('../lib')) .call(null, { shse: { caps }, @@ -76,7 +82,7 @@ test('createForMyself()', async (t) => { }) const { uri, url } = await p(local.invite.createForMyself)({ - _hubMsAddr: 'net:example.com:8008~shse:HUB_PUBKEY', + _hubMultiaddr: '/dns/example.com/tcp/8008/shse/HUB_PUBKEY', id: 'MOCK_ID', }) assert.equal( @@ -85,7 +91,7 @@ test('createForMyself()', async (t) => { ) assert.equal( url, - `http://example.com/invite#ppppp%3A%2F%2Finvite%2Fjoin%2Fdns%2Fexample.com%2Ftcp%2F8008%2Fshse%2FHUB_PUBKEY.MOCK_TOKEN%2Ftunnel-connect%2FHUB_PUBKEY%2F${local.shse.pubkey}%2Fpromise.account-add%2Faccount.MOCK_ID%2FMOCK_PROMISE` + `https://example.com/invite#ppppp%3A%2F%2Finvite%2Fjoin%2Fdns%2Fexample.com%2Ftcp%2F8008%2Fshse%2FHUB_PUBKEY.MOCK_TOKEN%2Ftunnel-connect%2FHUB_PUBKEY%2F${local.shse.pubkey}%2Fpromise.account-add%2Faccount.MOCK_ID%2FMOCK_PROMISE` ) assert.ok(connectCalled) diff --git a/test/parse.test.js b/test/parse.test.js index f97fe6d..7892131 100644 --- a/test/parse.test.js +++ b/test/parse.test.js @@ -21,7 +21,7 @@ test('parse() good friend invite', (t) => { assert.deepEqual(commands, [ { type: 'join', - address: 'net:example.com:8080~shse:PUBKEY:TOKEN', + multiaddr: '/dns/example.com/tcp/8080/shse/PUBKEY.TOKEN', }, { type: 'follow', @@ -42,11 +42,11 @@ test('parse() good myself invite', (t) => { assert.deepEqual(commands, [ { type: 'join', - address: 'net:example.com:8080~shse:PUBKEY:TOKEN', + multiaddr: '/dns/example.com/tcp/8080/shse/PUBKEY.TOKEN', }, { type: 'tunnel-connect', - address: 'tunnel:HUB_PUBKEY:OLD_PUBKEY~shse:OLD_PUBKEY', + multiaddr: '/tunnel/HUB_PUBKEY.OLD_PUBKEY/shse/OLD_PUBKEY', }, { type: 'promise.account-add', @@ -63,7 +63,7 @@ test('parse() good tokenless join invite', (t) => { assert.deepEqual(commands, [ { type: 'join', - address: 'net:example.com:8080~shse:PUBKEY', + multiaddr: '/dns/example.com/tcp/8080/shse/PUBKEY', }, ]) })