update to hubClient getting favorite hubs

This commit is contained in:
Andre Staltz 2024-01-16 13:58:32 +02:00
parent 14de10b5d3
commit 85737b4898
No known key found for this signature in database
GPG Key ID: 9EDE23EA7E8A4890
5 changed files with 166 additions and 110 deletions

View File

@ -1,16 +1,16 @@
// @ts-ignore // @ts-ignore
const MultiserverAddress = require('multiserver-address')
// @ts-ignore
const ip = require('ip') const ip = require('ip')
const p = require('promisify-tuple') const p = require('promisify-tuple')
/** /**
* @typedef {{ pubkey: string }} SHSE * @typedef {{ pubkey: string }} SHSE
* @typedef {ReturnType<import('ppppp-promise').init>} PPPPPPromise * @typedef {ReturnType<import('ppppp-promise').init>} PPPPPPromise
* @typedef {{connect: (addr: string, cb: CB<any>) => void}} PPPPPNet * @typedef {ReturnType<import('ppppp-net').init>} PPPPPNet
* @typedef {ReturnType<import('ppppp-hub-client/plugin').init>} PPPPPHubClient
* @typedef {import('ppppp-hub-client/plugin').HubMultiaddr} HubMultiaddr
* @typedef {{ * @typedef {{
* type: 'join', * type: 'join',
* address: string, * multiaddr: string,
* }} JoinCommand * }} JoinCommand
* @typedef {`join/${string}/${string}/${string}/${string}/${string}/${string}`} JoinCommandStr * @typedef {`join/${string}/${string}/${string}/${string}/${string}/${string}`} JoinCommandStr
* @typedef {{ * @typedef {{
@ -20,7 +20,7 @@ const p = require('promisify-tuple')
* @typedef {`follow/${string}`} FollowCommandStr * @typedef {`follow/${string}`} FollowCommandStr
* @typedef {{ * @typedef {{
* type: 'tunnel-connect', * type: 'tunnel-connect',
* address: string, * multiaddr: string,
* }} TunnelConnectCommand * }} TunnelConnectCommand
* @typedef {`tunnel-connect/${string}/${string}`} TunnelConnectCommandStr * @typedef {`tunnel-connect/${string}/${string}`} TunnelConnectCommandStr
* @typedef {{ * @typedef {{
@ -41,6 +41,12 @@ const p = require('promisify-tuple')
* | PromiseFollowCommand * | PromiseFollowCommand
* | PromiseAccountAddCommand * | PromiseAccountAddCommand
* } Command * } 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() pieces.shift()
pieces.shift() pieces.shift()
const shse = `shse:${cred.replace('.', ':')}` const multiaddr = `/${hostFormat}/${host}/${transport}/${port}/${transform}/${cred}`
const address = `net:${host}:${port}~${shse}` // TODO: add ws address here return { type: 'join', multiaddr }
return { type: 'join', address }
} }
/** /**
@ -137,8 +142,8 @@ function parseTunnelConnectCommand(pieces, uri) {
pieces.shift() pieces.shift()
pieces.shift() pieces.shift()
pieces.shift() pieces.shift()
const address = `tunnel:${hubPubkey}:${targetPubkey}~shse:${targetPubkey}` const multiaddr = `/tunnel/${hubPubkey}.${targetPubkey}/shse/${targetPubkey}`
return { type: 'tunnel-connect', address } return { type: 'tunnel-connect', multiaddr }
} }
/** /**
@ -240,11 +245,91 @@ function parse(uri) {
} }
/** /**
* @param {{ * Among the join commands, find the first valid hub, and return its
* shse: SHSE; * - web protocol (http or https)
* promise: PPPPPPromise; * - web hostname
* net: PPPPPNet; * - pubkey for shse
* }} peer * @param {Array<JoinCommandStr>} 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<JoinCommandStr>]>}
*/
async function makeJoinCommands(peer, amountHubs, hardcodedHub) {
/**@type {Array<JoinCommandStr>}*/
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 * @param {unknown} config
*/ */
function initInvite(peer, config) { function initInvite(peer, config) {
@ -252,7 +337,7 @@ function initInvite(peer, config) {
* @param {{ * @param {{
* hubs?: number, * hubs?: number,
* id: string, * id: string,
* _hubMsAddr?: string, * _hubMultiaddr?: HubMultiaddr
* }} opts * }} opts
* *
* @param {CB<{uri: string, url: string}>} cb * @param {CB<{uri: string, url: string}>} cb
@ -265,59 +350,37 @@ function initInvite(peer, config) {
// prettier-ignore // prettier-ignore
return cb(new Error(`invite.createForFriend opts.id is required for type "follow"`)) return cb(new Error(`invite.createForFriend opts.id is required for type "follow"`))
} }
const hubs = opts.hubs ?? 1 const amountHubs = opts.hubs ?? 1
if (typeof hubs !== 'number') { if (typeof amountHubs !== 'number') {
// prettier-ignore // 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) { // Create "join hub" commands
// prettier-ignore const [err1, joinCommands] = await makeJoinCommands(
// FIXME: load hubs from ppppp-net peer,
return cb(new Error(`invite.createForFriend expected opts._hubMsAddr because loading from connDB not yet supported`)) amountHubs,
} opts._hubMultiaddr
)
// Connect to hub and create token if (err1) return cb(err1)
const [err, rpc] = await p(peer.net.connect)(opts._hubMsAddr) const [protocol, hostname] = getFirstHub(joinCommands)
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 follow promise // Create follow promise
const [err3, token] = await p(peer.promise.create)({ const [err2, token] = await p(peer.promise.create)({
account: opts.id, account: opts.id,
type: 'follow', type: 'follow',
}) })
if (err3) return cb(err3) if (err2) return cb(err2)
/** @type {JoinCommandStr} */
const joinCommand = `join/${hostFormat}/${host}/tcp/${port}/shse/${pubkey}.${hubToken}`
/** @type {FollowCommandStr} */
const followCommand = `follow/${opts.id}`
/** @type {PromiseFollowCommandStr} */ /** @type {PromiseFollowCommandStr} */
const promiseCommand = `promise.follow/account.${opts.id}/${token}` const promiseCommand = `promise.follow/account.${opts.id}/${token}`
const uri = `ppppp://invite/${joinCommand}/${followCommand}/${promiseCommand}` // Create follow command
const url = `http://${host}/invite#${encodeURIComponent(uri)}` /** @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 }) cb(null, { uri, url })
} }
@ -325,7 +388,7 @@ function initInvite(peer, config) {
* @param {{ * @param {{
* hubs?: number, * hubs?: number,
* id: string, * id: string,
* _hubMsAddr?: string, * _hubMultiaddr?: HubMultiaddr
* }} opts * }} opts
* *
* @param {CB<{uri: string, url: string}>} cb * @param {CB<{uri: string, url: string}>} cb
@ -338,55 +401,35 @@ function initInvite(peer, config) {
// prettier-ignore // prettier-ignore
return cb(new Error(`invite.createForMyself opts.id is required for type "follow"`)) return cb(new Error(`invite.createForMyself opts.id is required for type "follow"`))
} }
const hubs = opts.hubs ?? 1 const amountHubs = opts.hubs ?? 1
if (typeof hubs !== 'number') { if (typeof amountHubs !== 'number') {
// prettier-ignore // 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) { // Create "join hub" commands
// prettier-ignore const [err1, joinCommands] = await makeJoinCommands(
return cb(new Error(`invite.createForMyself expected opts._hubMsAddr because loading from connDB not yet supported`)) peer,
} amountHubs,
opts._hubMultiaddr
// Connect to hub and create token )
const [err, rpc] = await p(peer.net.connect)(opts._hubMsAddr) if (err1) return cb(err1)
if (err) return cb(err) const [protocol, hostname, pubkey] = getFirstHub(joinCommands)
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 account-add promise // Create account-add promise
const promise = { type: 'account-add', account: opts.id } const promise = { type: 'account-add', account: opts.id }
const [err3, token] = await p(peer.promise.create)(promise) const [err3, token] = await p(peer.promise.create)(promise)
if (err3) return cb(err3) 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} */ /** @type {PromiseAccountAddCommandStr} */
const promiseCommand = `promise.account-add/account.${opts.id}/${token}` 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 }) cb(null, { uri, url })
} }
@ -394,7 +437,7 @@ function initInvite(peer, config) {
} }
exports.name = 'invite' exports.name = 'invite'
exports.needs = ['shse', 'promise', 'net'] exports.needs = ['shse', 'promise', 'net', 'hubClient']
exports.manifest = { exports.manifest = {
createForFriend: 'async', createForFriend: 'async',
createForMyself: 'async', createForMyself: 'async',

View File

@ -24,7 +24,6 @@
}, },
"dependencies": { "dependencies": {
"ip": "~1.1.8", "ip": "~1.1.8",
"multiserver-address": "~1.0.1",
"promisify-tuple": "1.2.0" "promisify-tuple": "1.2.0"
}, },
"devDependencies": { "devDependencies": {
@ -32,7 +31,9 @@
"c8": "^7.11.0", "c8": "^7.11.0",
"husky": "^4.3.0", "husky": "^4.3.0",
"ppppp-caps": "github:staltz/ppppp-caps", "ppppp-caps": "github:staltz/ppppp-caps",
"ppppp-hub-client": "github:staltz/ppppp-hub-client",
"ppppp-keypair": "github:staltz/ppppp-keypair", "ppppp-keypair": "github:staltz/ppppp-keypair",
"ppppp-net": "github:staltz/ppppp-net",
"ppppp-promise": "github:staltz/ppppp-promise", "ppppp-promise": "github:staltz/ppppp-promise",
"prettier": "^2.6.2", "prettier": "^2.6.2",
"pretty-quick": "^3.1.3", "pretty-quick": "^3.1.3",

View File

@ -25,7 +25,7 @@ test('createForFriend()', async (t) => {
return { return {
connect(address, cb) { connect(address, cb) {
connectCalled = true 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 = { const mockRpc = {
hub: { hub: {
createToken(cb) { createToken(cb) {
@ -56,11 +56,17 @@ test('createForFriend()', async (t) => {
}, },
} }
const mockHubClient = {
name: 'hubClient',
init() {}
}
const local = require('secret-stack/bare')() const local = require('secret-stack/bare')()
.use(require('secret-stack/plugins/net')) .use(require('secret-stack/plugins/net'))
.use(require('secret-handshake-ext/secret-stack')) .use(require('secret-handshake-ext/secret-stack'))
.use(mockNet) .use(mockNet)
.use(mockPromise) .use(mockPromise)
.use(mockHubClient)
.use(require('../lib')) .use(require('../lib'))
.call(null, { .call(null, {
shse: { shse: {
@ -78,7 +84,7 @@ test('createForFriend()', async (t) => {
}) })
const { uri, url } = await p(local.invite.createForFriend)({ 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', id: 'MOCK_ID',
}) })
assert.equal( assert.equal(
@ -87,7 +93,7 @@ test('createForFriend()', async (t) => {
) )
assert.equal( assert.equal(
url, 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) assert.ok(connectCalled)

View File

@ -25,7 +25,7 @@ test('createForMyself()', async (t) => {
return { return {
connect(address, cb) { connect(address, cb) {
connectCalled = true 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 = { const mockRpc = {
hub: { hub: {
createToken(cb) { createToken(cb) {
@ -56,11 +56,17 @@ test('createForMyself()', async (t) => {
}, },
} }
const mockHubClient = {
name: 'hubClient',
init() {},
}
const local = require('secret-stack/bare')() const local = require('secret-stack/bare')()
.use(require('secret-stack/plugins/net')) .use(require('secret-stack/plugins/net'))
.use(require('secret-handshake-ext/secret-stack')) .use(require('secret-handshake-ext/secret-stack'))
.use(mockNet) .use(mockNet)
.use(mockPromise) .use(mockPromise)
.use(mockHubClient)
.use(require('../lib')) .use(require('../lib'))
.call(null, { .call(null, {
shse: { caps }, shse: { caps },
@ -76,7 +82,7 @@ test('createForMyself()', async (t) => {
}) })
const { uri, url } = await p(local.invite.createForMyself)({ 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', id: 'MOCK_ID',
}) })
assert.equal( assert.equal(
@ -85,7 +91,7 @@ test('createForMyself()', async (t) => {
) )
assert.equal( assert.equal(
url, 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) assert.ok(connectCalled)

View File

@ -21,7 +21,7 @@ test('parse() good friend invite', (t) => {
assert.deepEqual(commands, [ assert.deepEqual(commands, [
{ {
type: 'join', type: 'join',
address: 'net:example.com:8080~shse:PUBKEY:TOKEN', multiaddr: '/dns/example.com/tcp/8080/shse/PUBKEY.TOKEN',
}, },
{ {
type: 'follow', type: 'follow',
@ -42,11 +42,11 @@ test('parse() good myself invite', (t) => {
assert.deepEqual(commands, [ assert.deepEqual(commands, [
{ {
type: 'join', type: 'join',
address: 'net:example.com:8080~shse:PUBKEY:TOKEN', multiaddr: '/dns/example.com/tcp/8080/shse/PUBKEY.TOKEN',
}, },
{ {
type: 'tunnel-connect', 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', type: 'promise.account-add',
@ -63,7 +63,7 @@ test('parse() good tokenless join invite', (t) => {
assert.deepEqual(commands, [ assert.deepEqual(commands, [
{ {
type: 'join', type: 'join',
address: 'net:example.com:8080~shse:PUBKEY', multiaddr: '/dns/example.com/tcp/8080/shse/PUBKEY',
}, },
]) ])
}) })