From cbafdaa2fcc84b6e6951c97a379ac72ec958e3ca Mon Sep 17 00:00:00 2001 From: Andre Staltz Date: Wed, 10 Jan 2024 18:50:15 +0200 Subject: [PATCH] update join command inspired by multiformats --- lib/index.js | 61 ++++++++++++++++++++++++++---------- package.json | 1 + protospec.md | 22 ++++++++++--- test/createForFriend.test.js | 4 +-- test/createForMyself.test.js | 4 +-- test/parse.test.js | 24 ++++++++++---- 6 files changed, 85 insertions(+), 31 deletions(-) diff --git a/lib/index.js b/lib/index.js index d24cdfa..e898efa 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,5 +1,7 @@ // @ts-ignore const MultiserverAddress = require('multiserver-address') +// @ts-ignore +const ip = require('ip') const p = require('promisify-tuple') /** @@ -10,7 +12,7 @@ const p = require('promisify-tuple') * type: 'join', * address: string, * }} JoinCommand - * @typedef {`join/${string}/${string}/${string}/${string}`} JoinCommandStr + * @typedef {`join/${string}/${string}/${string}/${string}/${string}/${string}`} JoinCommandStr * @typedef {{ * type: 'follow', * id: string, @@ -52,32 +54,49 @@ const p = require('promisify-tuple') * @returns {JoinCommand} */ function parseJoinCommand(pieces, uri) { - const [, host, port, pubkey, token] = pieces - if (!host) { + if (pieces.length < 7) { // prettier-ignore - throw new Error(`Invalid URI "${uri}" for invite.parse, missing join hub address`) + throw new Error(`Invalid URI "${uri}" for invite.parse, missing some "join" arguments`) } - // TODO: numeric validation for the port - if (!port) { + const [, hostFormat, host, transport, port, transform, cred] = pieces // pubkey, token] = pieces + if (hostFormat !== 'ip4' && hostFormat !== 'ip6' && hostFormat !== 'dns') { // prettier-ignore - throw new Error(`Invalid URI "${uri}" for invite.parse, missing join hub port`) + throw new Error(`Invalid URI "${uri}" for invite.parse, unsupported "join" host format "${hostFormat}"`) } - // TODO: base58 validation for the pubkey (and maybe length) - if (!pubkey) { + if ( + (hostFormat === 'ip4' && !ip.isV4Format(host)) || + (hostFormat === 'ip6' && !ip.isV6Format(host)) + ) { // prettier-ignore - throw new Error(`Invalid URI "${uri}" for invite.parse, missing join hub pubkey`) + throw new Error(`Invalid URI "${uri}" for invite.parse, incoherent "join" host "${hostFormat}/${host}"`) } - // TODO: base58 validation for the token, if present at all - if (!token) { + if (hostFormat === 'dns' && !host.includes('.')) { // prettier-ignore - throw new Error(`Invalid URI "${uri}" for invite.parse, missing join hub token`) + throw new Error(`Invalid URI "${uri}" for invite.parse, invalid "join" host "${hostFormat}/${host}"`) } + if (transport !== 'tcp') { + // prettier-ignore + throw new Error(`Invalid URI "${uri}" for invite.parse, unsupported "join" transport "${transport}"`) + } + const portNum = parseInt(port) + if (isNaN(portNum) || portNum < 0 || portNum > 65535) { + // prettier-ignore + throw new Error(`Invalid URI "${uri}" for invite.parse, invalid "join" port ${port}`) + } + if (transform !== 'shse') { + // prettier-ignore + throw new Error(`Invalid URI "${uri}" for invite.parse, unsupported "join" transform "${transform}"`) + } + // TODO: base58 validation for the shse pubkey (and maybe length) + // TODO: base58 validation for the shse token, if present at all pieces.shift() pieces.shift() pieces.shift() pieces.shift() pieces.shift() - const shse = token === 'none' ? `shse:${pubkey}` : `shse:${pubkey}:${token}` + pieces.shift() + pieces.shift() + const shse = `shse:${cred.replace('.', ':')}` const address = `net:${host}:${port}~${shse}` // TODO: add ws address here return { type: 'join', address } } @@ -273,6 +292,11 @@ function initInvite(peer, config) { 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 @@ -282,7 +306,7 @@ function initInvite(peer, config) { if (err3) return cb(err3) /** @type {JoinCommandStr} */ - const joinCommand = `join/${host}/${port}/${pubkey}/${hubToken}` + const joinCommand = `join/${hostFormat}/${host}/tcp/${port}/shse/${pubkey}.${hubToken}` /** @type {FollowCommandStr} */ const followCommand = `follow/${opts.id}` /** @type {PromiseFollowCommandStr} */ @@ -337,6 +361,11 @@ function initInvite(peer, config) { 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 @@ -347,7 +376,7 @@ function initInvite(peer, config) { if (err3) return cb(err3) /** @type {JoinCommandStr} */ - const joinCommand = `join/${host}/${port}/${pubkey}/${hubToken}` + const joinCommand = `join/${hostFormat}/${host}/tcp/${port}/shse/${pubkey}.${hubToken}` /** @type {TunnelConnectCommandStr} */ const tunnelCommand = `tunnel-connect/${pubkey}/${peer.shse.pubkey}` /** @type {PromiseAccountAddCommandStr} */ diff --git a/package.json b/package.json index 5e74493..d50351d 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "node": ">=16" }, "dependencies": { + "ip": "~1.1.8", "multiserver-address": "~1.0.1", "promisify-tuple": "1.2.0" }, diff --git a/protospec.md b/protospec.md index b6a5684..a0c6d24 100644 --- a/protospec.md +++ b/protospec.md @@ -5,13 +5,19 @@ **Invite URL:** ``` -ppppp://invite/join/HOST/PORT/PUBKEY/TOKEN/follow/ALICE_ID/promise.follow/account.ALICE_ID/ALICE_TOKEN +ppppp://invite/join/HOSTFORMAT/HOST/TRANSPORT/PORT/TRANSFORM/CREDENTIALS/follow/ALICE_ID/promise.follow/account.ALICE_ID/ALICE_TOKEN ``` made of 3 "commands": -- `join/HOST/PORT/PUBKEY/TOKEN` +- `join/HOSTFORMAT/HOST/TRANSPORT/PORT/TRANSFORM/CREDENTIALS` - Meaning "join" this hub at this address, claiming this token to become a member + - `HOSTFORMAT` is `ip4` or `ip6` or `dns` + - `HOST` is the host address + - `TRANSPORT` is `tcp` (or others to be supported in the future) + - `PORT` is the port number + - `TRANSFORM` is `shse` (or others to be supported in the future) + - `CREDENTIALS` is `PUBKEY.TOKEN` where PUBKEY is the hub's public key and TOKEN is the hub membership token to claim - `follow/ALICE_ID` - Meaning that you should follow Alice - `promise.follow/account.ALICE_ID/ALICE_TOKEN` @@ -64,13 +70,19 @@ end **Invite URL:** ``` -ppppp://invite/join/HOST/PORT/PUBKEY/TOKEN/tunnel-connect/HUB_PUBKEY/OLD_PUBKEY/promise.account-add/peer.PUBKEY/OLD_TOKEN/promise.account-internal-encryption-key/peer.PUBKEY/OLD_TOKEN +ppppp://invite/join/HOSTFORMAT/HOST/TRANSPORT/PORT/TRANSFORM/CREDENTIALS/tunnel-connect/HUB_PUBKEY/OLD_PUBKEY/promise.account-add/peer.PUBKEY/OLD_TOKEN/promise.account-internal-encryption-key/peer.PUBKEY/OLD_TOKEN ``` made of 3 "commands": -- `join/HOST/PORT/PUBKEY/TOKEN` - - Meaning "join" this hub at this address, claiming this token +- `join/HOSTFORMAT/HOST/TRANSPORT/PORT/TRANSFORM/CREDENTIALS` + - Meaning "join" this hub at this address, claiming this token to become a member + - `HOSTFORMAT` is `ip4` or `ip6` or `dns` + - `HOST` is the host address + - `TRANSPORT` is `tcp` (or others to be supported in the future) + - `PORT` is the port number + - `TRANSFORM` is `shse` (or others to be supported in the future) + - `CREDENTIALS` is `PUBKEY.TOKEN` where PUBKEY is the hub's public key and TOKEN is the hub membership token to claim - `tunnel-connect/HUB_PUBKEY/OLD_PUBKEY` - Meaning that you should connect to the old device via a tunnel in the hub - `promise.account-add/peer.PUBKEY/OLD_TOKEN` TODO implement with peer.PUBKEY diff --git a/test/createForFriend.test.js b/test/createForFriend.test.js index bc60f4e..bdde02f 100644 --- a/test/createForFriend.test.js +++ b/test/createForFriend.test.js @@ -83,11 +83,11 @@ test('createForFriend()', async (t) => { }) assert.equal( uri, - `ppppp://invite/join/example.com/8008/HUB_PUBKEY/MOCK_TOKEN/follow/MOCK_ID/promise.follow/account.MOCK_ID/MOCK_PROMISE` + `ppppp://invite/join/dns/example.com/tcp/8008/shse/HUB_PUBKEY.MOCK_TOKEN/follow/MOCK_ID/promise.follow/account.MOCK_ID/MOCK_PROMISE` ) assert.equal( url, - `http://example.com/invite#ppppp%3A%2F%2Finvite%2Fjoin%2Fexample.com%2F8008%2FHUB_PUBKEY%2FMOCK_TOKEN%2Ffollow%2FMOCK_ID%2Fpromise.follow%2Faccount.MOCK_ID%2FMOCK_PROMISE` + `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` ) assert.ok(connectCalled) diff --git a/test/createForMyself.test.js b/test/createForMyself.test.js index f1df8d4..091253e 100644 --- a/test/createForMyself.test.js +++ b/test/createForMyself.test.js @@ -81,11 +81,11 @@ test('createForMyself()', async (t) => { }) assert.equal( uri, - `ppppp://invite/join/example.com/8008/HUB_PUBKEY/MOCK_TOKEN/tunnel-connect/HUB_PUBKEY/${local.shse.pubkey}/promise.account-add/account.MOCK_ID/MOCK_PROMISE` + `ppppp://invite/join/dns/example.com/tcp/8008/shse/HUB_PUBKEY.MOCK_TOKEN/tunnel-connect/HUB_PUBKEY/${local.shse.pubkey}/promise.account-add/account.MOCK_ID/MOCK_PROMISE` ) assert.equal( url, - `http://example.com/invite#ppppp%3A%2F%2Finvite%2Fjoin%2Fexample.com%2F8008%2FHUB_PUBKEY%2FMOCK_TOKEN%2Ftunnel-connect%2FHUB_PUBKEY%2F${local.shse.pubkey}%2Fpromise.account-add%2Faccount.MOCK_ID%2FMOCK_PROMISE` + `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` ) assert.ok(connectCalled) diff --git a/test/parse.test.js b/test/parse.test.js index d331527..f97fe6d 100644 --- a/test/parse.test.js +++ b/test/parse.test.js @@ -4,24 +4,24 @@ const plugin = require('../lib/index') test('parse() error cases', (t) => { assert.throws(() => { - plugin.parse('ssb://invite/join/HUB_ADDR/HUB_PUBKEY/HUB_TOKEN') + plugin.parse('ssb://invite/join/ip4/127.0.0.1/tcp/HUB_PUBKEY/HUB_TOKEN') }) assert.throws(() => { plugin.parse('ppppp:invite') }) assert.throws(() => { - plugin.parse('ppppp:invite/join/HUB_ADDR') + plugin.parse('ppppp:invite/join/ip4/127.0.0.1') }) }) test('parse() good friend invite', (t) => { const commands = plugin.parse( - 'ppppp://invite/join/HOST/PORT/PUBKEY/TOKEN/follow/ALICE/promise.follow/account.ALICE/ALICE_TOKEN' + 'ppppp://invite/join/dns/example.com/tcp/8080/shse/PUBKEY.TOKEN/follow/ALICE/promise.follow/account.ALICE/ALICE_TOKEN' ) assert.deepEqual(commands, [ { type: 'join', - address: 'net:HOST:PORT~shse:PUBKEY:TOKEN', + address: 'net:example.com:8080~shse:PUBKEY:TOKEN', }, { type: 'follow', @@ -37,12 +37,12 @@ test('parse() good friend invite', (t) => { test('parse() good myself invite', (t) => { const commands = plugin.parse( - 'ppppp://invite/join/HOST/PORT/PUBKEY/TOKEN/tunnel-connect/HUB_PUBKEY/OLD_PUBKEY/promise.account-add/account.ACCOUNT_ID/OLD_TOKEN' + 'ppppp://invite/join/dns/example.com/tcp/8080/shse/PUBKEY.TOKEN/tunnel-connect/HUB_PUBKEY/OLD_PUBKEY/promise.account-add/account.ACCOUNT_ID/OLD_TOKEN' ) assert.deepEqual(commands, [ { type: 'join', - address: 'net:HOST:PORT~shse:PUBKEY:TOKEN', + address: 'net:example.com:8080~shse:PUBKEY:TOKEN', }, { type: 'tunnel-connect', @@ -55,3 +55,15 @@ test('parse() good myself invite', (t) => { }, ]) }) + +test('parse() good tokenless join invite', (t) => { + const commands = plugin.parse( + 'ppppp://invite/join/dns/example.com/tcp/8080/shse/PUBKEY' + ) + assert.deepEqual(commands, [ + { + type: 'join', + address: 'net:example.com:8080~shse:PUBKEY', + }, + ]) +})