From fe4612a37620203a04b70ec96acce7c890f2ec7d Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Tue, 28 Mar 2023 07:13:13 -0700 Subject: [PATCH] fix: allow dialling ip6 webtransport addresses (#60) use valid rfc3986 ipv6 host syntax --------- Co-authored-by: achingbrain --- .aegir.js | 50 ++++++++++++++++----------- go-libp2p-webtransport-server/main.go | 5 +++ package.json | 3 +- src/index.ts | 17 +++++++-- test/browser.ts | 43 +++++++++++++++++++---- 5 files changed, 88 insertions(+), 30 deletions(-) diff --git a/.aegir.js b/.aegir.js index fa6dfdb..a31c770 100644 --- a/.aegir.js +++ b/.aegir.js @@ -1,48 +1,58 @@ -import { spawn, exec } from "child_process"; -import { existsSync } from "fs"; +import { spawn, exec } from 'child_process' +import { existsSync } from 'fs' +import defer from 'p-defer' /** @type {import('aegir/types').PartialOptions} */ export default { test: { async before() { - if (!existsSync("./go-libp2p-webtransport-server/main")) { + if (!existsSync('./go-libp2p-webtransport-server/main')) { await new Promise((resolve, reject) => { exec('go build -o main main.go', - { cwd: "./go-libp2p-webtransport-server" }, + { cwd: './go-libp2p-webtransport-server' }, (error, stdout, stderr) => { if (error) { reject(error) - console.error(`exec error: ${error}`); - return; + console.error(`exec error: ${error}`) + return } resolve() - }); + }) }) } - const server = spawn('./main', [], { cwd: "./go-libp2p-webtransport-server", killSignal: "SIGINT" }); + const server = spawn('./main', [], { cwd: './go-libp2p-webtransport-server', killSignal: 'SIGINT' }) server.stderr.on('data', (data) => { - console.log(`stderr: ${data}`, typeof data); + console.log('stderr:', data.toString()) + }) + const serverAddr = defer() + const serverAddr6 = defer() + + server.stdout.on('data', (buf) => { + const data = buf.toString() + + console.log('stdout:', data); + if (data.includes('addr=/ip4')) { + // Parse the addr out + serverAddr.resolve(`/ip4${data.match(/addr=\/ip4(.*)/)[1]}`) + } + + if (data.includes('addr=/ip6')) { + // Parse the addr out + serverAddr6.resolve(`/ip6${data.match(/addr=\/ip6(.*)/)[1]}`) + } }) - const serverAddr = await (new Promise((resolve => { - server.stdout.on('data', (data) => { - console.log(`stdout: ${data}`, typeof data); - if (data.includes("addr=")) { - // Parse the addr out - resolve((data + "").match(/addr=([^\s]*)/)[1]) - } - }); - }))) return { server, env: { - serverAddr + serverAddr: await serverAddr.promise, + serverAddr6: await serverAddr6.promise } } }, async after(_, { server }) { - server.kill("SIGINT") + server.kill('SIGINT') } }, build: { diff --git a/go-libp2p-webtransport-server/main.go b/go-libp2p-webtransport-server/main.go index 1ffe809..6a388e2 100644 --- a/go-libp2p-webtransport-server/main.go +++ b/go-libp2p-webtransport-server/main.go @@ -24,6 +24,11 @@ func main() { panic(err) } + err = h.Network().Listen(multiaddr.StringCast("/ip6/::1/udp/0/quic-v1/webtransport")) + if err != nil { + panic(err) + } + h.SetStreamHandler("echo", func(s network.Stream) { io.Copy(s, s) s.Close() diff --git a/package.json b/package.json index 0c3779f..b95041c 100644 --- a/package.json +++ b/package.json @@ -169,7 +169,8 @@ }, "devDependencies": { "aegir": "^38.1.7", - "libp2p": "^0.43.2" + "libp2p": "^0.43.2", + "p-defer": "^4.0.0" }, "browser": { "./dist/src/listener.js": "./dist/src/listener.browser.js" diff --git a/src/index.ts b/src/index.ts index e54c01a..418a1b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -186,10 +186,23 @@ function parseMultiaddr (ma: Multiaddr): { url: string, certhashes: MultihashDig // eslint-disable-next-line complexity const { url, certhashes, remotePeer } = parts.reduce((state: { url: string, certhashes: MultihashDigest[], seenHost: boolean, seenPort: boolean, remotePeer?: PeerId }, [proto, value]) => { switch (proto) { - case protocols('ip4').code: case protocols('ip6').code: - case protocols('dns4').code: + // @ts-expect-error - ts error on switch fallthrough case protocols('dns6').code: + if (value?.includes(':') === true) { + /** + * This resolves cases where `new globalThis.WebTransport` fails to construct because of an invalid URL being passed. + * + * `new URL('https://::1:4001/blah')` will throw a `TypeError: Failed to construct 'URL': Invalid URL` + * `new URL('https://[::1]:4001/blah')` is valid and will not. + * + * @see https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2 + */ + value = `[${value}]` + } + // eslint-disable-next-line no-fallthrough + case protocols('ip4').code: + case protocols('dns4').code: if (state.seenHost || state.seenPort) { throw new Error('Invalid multiaddr, saw host and already saw the host or port') } diff --git a/test/browser.ts b/test/browser.ts index f466c7b..5945f4b 100644 --- a/test/browser.ts +++ b/test/browser.ts @@ -15,8 +15,11 @@ declare global { describe('libp2p-webtransport', () => { it('webtransport connects to go-libp2p', async () => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const maStr: string = process.env.serverAddr! + if (process.env.serverAddr == null) { + throw new Error('serverAddr not found') + } + + const maStr: string = process.env.serverAddr const ma = multiaddr(maStr) const node = await createLibp2p({ transports: [webTransport()], @@ -71,8 +74,11 @@ describe('libp2p-webtransport', () => { }) it('fails to connect without certhashes', async () => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const maStr: string = process.env.serverAddr! + if (process.env.serverAddr == null) { + throw new Error('serverAddr not found') + } + + const maStr: string = process.env.serverAddr const maStrNoCerthash: string = maStr.split('/certhash')[0] const maStrP2p = maStr.split('/p2p/')[1] const ma = multiaddr(maStrNoCerthash + '/p2p/' + maStrP2p) @@ -89,10 +95,33 @@ describe('libp2p-webtransport', () => { await node.stop() }) + it('connects to ipv6 addresses', async () => { + if (process.env.serverAddr6 == null) { + throw new Error('serverAddr6 not found') + } + + const ma = multiaddr(process.env.serverAddr6) + const node = await createLibp2p({ + transports: [webTransport()], + connectionEncryption: [noise()] + }) + + await node.start() + + // the address is unreachable but we can parse it correctly + const stream = await node.dialProtocol(ma, '/ipfs/ping/1.0.0') + stream.close() + + await node.stop() + }) + it('Closes writes of streams after they have sunk a source', async () => { - // This is the behavor of stream muxers: (see mplex, yamux and compliance tests: https://github.com/libp2p/js-libp2p-interfaces/blob/master/packages/interface-stream-muxer-compliance-tests/src/close-test.ts) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const maStr: string = process.env.serverAddr! + // This is the behavior of stream muxers: (see mplex, yamux and compliance tests: https://github.com/libp2p/js-libp2p-interfaces/blob/master/packages/interface-stream-muxer-compliance-tests/src/close-test.ts) + if (process.env.serverAddr == null) { + throw new Error('serverAddr not found') + } + + const maStr: string = process.env.serverAddr const ma = multiaddr(maStr) const node = await createLibp2p({ transports: [webTransport()],