diff --git a/package.json b/package.json index 299c3f9..b82e069 100644 --- a/package.json +++ b/package.json @@ -148,4 +148,4 @@ "devDependencies": { "aegir": "^38.1.7" } -} +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 306bd89..37f8dbb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,74 +1,203 @@ -import type { Multiaddr } from '@multiformats/multiaddr' -import { multiaddr } from '@multiformats/multiaddr' +import type { Multiaddr, StringTuple } from '@multiformats/multiaddr' +import { multiaddr, protocols } from '@multiformats/multiaddr' export interface MultiaddrToUriOpts { assumeHttp?: boolean } -interface Reducer { (str: string, content: string, i: number, parts: Part[], opts?: MultiaddrToUriOpts): string } +interface Interpreter { (value: string, ma: StringTuple[]): string } -const reduceValue: Reducer = (_, v) => v -const tcpUri = (str: string, port: string, parts: Part[], opts?: MultiaddrToUriOpts): string => { - // return tcp when explicitly requested - if ((opts != null) && opts.assumeHttp === false) return `tcp://${str}:${port}` - // check if tcp is the last protocol in multiaddr - let protocol = 'tcp' - let explicitPort = `:${port}` - const last = parts[parts.length - 1] - if (last.protocol === 'tcp') { - // assume http and produce clean urls - protocol = port === '443' ? 'https' : 'http' - explicitPort = port === '443' || port === '80' ? '' : explicitPort +function extractSNI (ma: StringTuple[]): string | null { + let sniProtoCode: number + try { + sniProtoCode = protocols('sni').code + } catch (e) { + // No SNI protocol in multiaddr + return null } - return `${protocol}://${str}${explicitPort}` + for (const [proto, value] of ma) { + if (proto === sniProtoCode && value !== undefined) { + return value + } + } + return null } -const Reducers: Record = { - ip4: reduceValue, - ip6: (str, content, i, parts) => ( - parts.length === 1 && parts[0].protocol === 'ip6' - ? content - : `[${content}]` - ), - tcp: (str, content, i, parts, opts) => ( - parts.some(p => ['http', 'https', 'ws', 'wss'].includes(p.protocol)) - ? `${str}:${content}` - : tcpUri(str, content, parts, opts) - ), - udp: (str, content) => `udp://${str}:${content}`, - dnsaddr: reduceValue, - dns4: reduceValue, - dns6: reduceValue, - ipfs: (str, content) => `${str}/ipfs/${content}`, - p2p: (str, content) => `${str}/p2p/${content}`, - http: str => `http://${str}`, - https: str => `https://${str}`, - ws: str => `ws://${str}`, - wss: str => `wss://${str}`, - 'p2p-websocket-star': str => `${str}/p2p-websocket-star`, - 'p2p-webrtc-star': str => `${str}/p2p-webrtc-star`, - 'p2p-webrtc-direct': str => `${str}/p2p-webrtc-direct` +function hasTLS (ma: StringTuple[]): boolean { + return ma.some(([proto, _]) => proto === protocols('tls').code) } -interface Part { - protocol: string - content: string +function interpretNext (headProtoCode: number, headProtoVal: string, restMa: StringTuple[]): string { + const interpreter = interpreters[protocols(headProtoCode).name] + if (interpreter === undefined) { + throw new Error(`Can't interpret protocol ${protocols(headProtoCode).name}`) + } + const restVal = interpreter(headProtoVal, restMa) + if (headProtoCode === protocols('ip6').code) { + return `[${restVal}]` + } + return restVal +} + +const interpreters: Record = { + ip4: (value: string, restMa: StringTuple[]) => value, + ip6: (value: string, restMa: StringTuple[]) => { + if (restMa.length === 0) { + return value + } + return `[${value}]` + }, + tcp: (value: string, restMa: StringTuple[]) => { + const tailProto = restMa.pop() + if (tailProto === undefined) { + throw new Error('Unexpected end of multiaddr') + } + return `tcp://${interpretNext(tailProto[0], tailProto[1] ?? '', restMa)}:${value}` + }, + udp: (value: string, restMa: StringTuple[]) => { + const tailProto = restMa.pop() + if (tailProto === undefined) { + throw new Error('Unexpected end of multiaddr') + } + return `udp://${interpretNext(tailProto[0], tailProto[1] ?? '', restMa)}:${value}` + }, + dnsaddr: (value: string, restMa: StringTuple[]) => value, + dns4: (value: string, restMa: StringTuple[]) => value, + dns6: (value: string, restMa: StringTuple[]) => value, + ipfs: (value: string, restMa: StringTuple[]) => { + const tailProto = restMa.pop() + if (tailProto === undefined) { + throw new Error('Unexpected end of multiaddr') + } + return `${interpretNext(tailProto[0], tailProto[1] ?? '', restMa)}/ipfs/${value}` + }, + p2p: (value: string, restMa: StringTuple[]) => { + const tailProto = restMa.pop() + if (tailProto === undefined) { + throw new Error('Unexpected end of multiaddr') + } + return `${interpretNext(tailProto[0], tailProto[1] ?? '', restMa)}/p2p/${value}` + }, + http: (value: string, restMa: StringTuple[]) => { + const maHasTLS = hasTLS(restMa) + const sni = extractSNI(restMa) + if (maHasTLS && sni !== null) { + return `https://${sni}` + } + const protocol = maHasTLS ? 'https://' : 'http://' + const tailProto = restMa.pop() + if (tailProto === undefined) { + throw new Error('Unexpected end of multiaddr') + } + let baseVal = interpretNext(tailProto[0], tailProto[1] ?? '', restMa) + // We are reinterpreting the base as http, so we need to remove the tcp:// if it's there + baseVal = baseVal.replace('tcp://', '') + return `${protocol}${baseVal}` + }, + tls: (value: string, restMa: StringTuple[]) => { + // Noop, the parent context knows that it's tls. We don't need to do + // anything here + const tailProto = restMa.pop() + if (tailProto === undefined) { + throw new Error('Unexpected end of multiaddr') + } + return interpretNext(tailProto[0], tailProto[1] ?? '', restMa) + }, + sni: (value: string, restMa: StringTuple[]) => { + // Noop, the parent context uses the sni information, we don't need to do + // anything here + const tailProto = restMa.pop() + if (tailProto === undefined) { + throw new Error('Unexpected end of multiaddr') + } + return interpretNext(tailProto[0], tailProto[1] ?? '', restMa) + }, + https: (value: string, restMa: StringTuple[]) => { + const tailProto = restMa.pop() + if (tailProto === undefined) { + throw new Error('Unexpected end of multiaddr') + } + let baseVal = interpretNext(tailProto[0], tailProto[1] ?? '', restMa) + // We are reinterpreting the base as http, so we need to remove the tcp:// if it's there + baseVal = baseVal.replace('tcp://', '') + return `https://${baseVal}` + }, + ws: (value: string, restMa: StringTuple[]) => { + const maHasTLS = hasTLS(restMa) + const sni = extractSNI(restMa) + if (maHasTLS && sni !== null) { + return `wss://${sni}` + } + const protocol = maHasTLS ? 'wss://' : 'ws://' + const tailProto = restMa.pop() + if (tailProto === undefined) { + throw new Error('Unexpected end of multiaddr') + } + let baseVal = interpretNext(tailProto[0], tailProto[1] ?? '', restMa) + // We are reinterpreting the base, so we need to remove the tcp:// if it's there + baseVal = baseVal.replace('tcp://', '') + return `${protocol}${baseVal}` + }, + wss: (value: string, restMa: StringTuple[]) => { + const tailProto = restMa.pop() + if (tailProto === undefined) { + throw new Error('Unexpected end of multiaddr') + } + let baseVal = interpretNext(tailProto[0], tailProto[1] ?? '', restMa) + // We are reinterpreting the base as http, so we need to remove the tcp:// if it's there + baseVal = baseVal.replace('tcp://', '') + return `wss://${baseVal}` + }, + 'p2p-websocket-star': (value: string, restMa: StringTuple[]) => { + const tailProto = restMa.pop() + if (tailProto === undefined) { + throw new Error('Unexpected end of multiaddr') + } + return `${interpretNext(tailProto[0], tailProto[1] ?? '', restMa)}/p2p-websocket-star` + }, + 'p2p-webrtc-star': (value: string, restMa: StringTuple[]) => { + const tailProto = restMa.pop() + if (tailProto === undefined) { + throw new Error('Unexpected end of multiaddr') + } + return `${interpretNext(tailProto[0], tailProto[1] ?? '', restMa)}/p2p-webrtc-star` + }, + 'p2p-webrtc-direct': (value: string, restMa: StringTuple[]) => { + const tailProto = restMa.pop() + if (tailProto === undefined) { + throw new Error('Unexpected end of multiaddr') + } + return `${interpretNext(tailProto[0], tailProto[1] ?? '', restMa)}/p2p-webrtc-direct` + } } export function multiaddrToUri (input: Multiaddr | string | Uint8Array, opts?: MultiaddrToUriOpts): string { const ma = multiaddr(input) - const parts = ma.toString().split('/').slice(1) - return ma - .tuples() - .map(tuple => ({ - protocol: parts.shift() ?? '', - content: (tuple[1] != null) ? parts.shift() ?? '' : '' - })) - .reduce((str: string, part: Part, i: number, parts: Part[]) => { - const reduce = Reducers[part.protocol] - if (reduce == null) { - throw new Error(`Unsupported protocol ${part.protocol}`) + const parts = ma.stringTuples() + const head = parts.pop() + if (head === undefined) { + throw new Error('Unexpected end of multiaddr') + } + + const protocol = protocols(head[0]) + const interpreter = interpreters[protocol.name] + + if (interpreter == null) { + throw new Error(`No interpreter found for ${protocol.name}`) + } + + let uri = interpreter(head[1] ?? '', parts) + if (opts?.assumeHttp !== false && head[0] === protocols('tcp').code) { + // If rightmost proto is tcp, we assume http here + uri = uri.replace('tcp://', 'http://') + if (head[1] === '443' || head[1] === '80') { + if (head[1] === '443') { + uri = uri.replace('http://', 'https://') } - return reduce(str, part.content, i, parts, opts) - }, '') + // Drop the port + uri = uri.substring(0, uri.lastIndexOf(':')) + } + } + + return uri } diff --git a/test/test.spec.ts b/test/test.spec.ts index 73e7d3a..479e8ec 100644 --- a/test/test.spec.ts +++ b/test/test.spec.ts @@ -11,6 +11,8 @@ describe('multiaddr-to-uri', () => { ['/ip6/fc00::/http', 'http://[fc00::]'], ['/ip4/0.0.7.6/tcp/1234/http', 'http://0.0.7.6:1234'], ['/ip4/0.0.7.6/tcp/1234/https', 'https://0.0.7.6:1234'], + ['/ip4/0.0.7.6/tcp/1234/tls/http', 'https://0.0.7.6:1234'], + ['/ip4/1.2.3.4/tcp/1234/tls/sni/ipfs.io/http', 'https://ipfs.io'], ['/ip4/0.0.7.6/udp/1234', 'udp://0.0.7.6:1234'], ['/ip6/::/udp/0', 'udp://[::]:0'], ['/dnsaddr/ipfs.io', 'ipfs.io'], @@ -25,6 +27,8 @@ describe('multiaddr-to-uri', () => { ['/ip4/1.2.3.4/tcp/3456/ws', 'ws://1.2.3.4:3456'], ['/ip6/::/tcp/0/ws', 'ws://[::]:0'], ['/dnsaddr/ipfs.io/wss', 'wss://ipfs.io'], + ['/dnsaddr/ipfs.io/tls/ws', 'wss://ipfs.io'], + ['/ip4/1.2.3.4/tcp/1234/tls/sni/ipfs.io/ws', 'wss://ipfs.io'], ['/ip4/1.2.3.4/tcp/3456/wss', 'wss://1.2.3.4:3456'], ['/ip6/::/tcp/0/wss', 'wss://[::]:0'], [