diff --git a/lib/multiaddr.js b/lib/multiaddr.js index b38d9a4..64d9fc8 100644 --- a/lib/multiaddr.js +++ b/lib/multiaddr.js @@ -3,13 +3,31 @@ * @returns {string} Parsed URI, e.g. `http://127.0.0.1:80` */ export function multiaddrToHttpUrl (addr) { - const [, hostType, hostValue, ipProtocol, port, scheme, ...rest] = addr.split('/') + const [multiAddr, httpPathMultiAddr] = addr.split('/http-path') + const [, hostType, hostValue, ...multiAddrParts] = multiAddr.split('/') + let scheme, path, rest, port + if (addr.includes('/http-path')) { + [scheme, ...rest] = multiAddrParts + try { + // Remove leading slash and parse URI-encoded path + // See https://github.com/multiformats/multiaddr/blob/cab92e8e6da2e70c5f1b8aa59976e71e6922b392/protocols/http-path.md#usage + path = decodeURIComponent(httpPathMultiAddr.substring(1)) + } catch (err) { + throw Object.assign( + new Error(`Cannot parse "${addr}": unsupported http path`, { cause: err }), + { code: 'INVALID_HTTP_PATH' } + ) + } + } else { + let ipProtocol + ;[ipProtocol, port, scheme, ...rest] = multiAddrParts - if (ipProtocol !== 'tcp') { - throw Object.assign( - new Error(`Cannot parse "${addr}": unsupported protocol "${ipProtocol}"`), - { code: 'UNSUPPORTED_MULTIADDR_PROTO' } - ) + if (ipProtocol !== 'tcp') { + throw Object.assign( + new Error(`Cannot parse "${addr}": unsupported protocol "${ipProtocol}"`), + { code: 'UNSUPPORTED_MULTIADDR_PROTO' } + ) + } } if (scheme !== 'http' && scheme !== 'https') { @@ -26,7 +44,10 @@ export function multiaddrToHttpUrl (addr) { ) } - return `${scheme}://${getUriHost(hostType, hostValue)}${getUriPort(scheme, port)}` + let url = `${scheme}://${getUriHost(hostType, hostValue)}` + if (port) url += getUriPort(scheme, port) + if (path) url += path + return url } function getUriHost (hostType, hostValue) { diff --git a/lib/spark.js b/lib/spark.js index 80feaf8..a429ab5 100644 --- a/lib/spark.js +++ b/lib/spark.js @@ -341,6 +341,8 @@ function mapErrorToStatusCode (err) { return 703 case 'MULTIADDR_HAS_TOO_MANY_PARTS': return 704 + case 'INVALID_HTTP_PATH': + return 705 } // 9xx for content verification errors diff --git a/test/multiaddr.test.js b/test/multiaddr.test.js index 4a167ac..ad491d8 100644 --- a/test/multiaddr.test.js +++ b/test/multiaddr.test.js @@ -9,7 +9,10 @@ const HAPPY_CASES = [ ['/ip4/127.0.0.1/tcp/8080/https', 'https://127.0.0.1:8080'], ['/dns/meridian.space/tcp/8080/http', 'http://meridian.space:8080'], ['/dns4/meridian.space/tcp/8080/http', 'http://meridian.space:8080'], - ['/dns6/meridian.space/tcp/8080/http', 'http://meridian.space:8080'] + ['/dns6/meridian.space/tcp/8080/http', 'http://meridian.space:8080'], + ['/dns/meridian.space/https/http-path/%2Fipni-provider%2FproviderID', 'https://meridian.space/ipni-provider/providerID'], + ['/dns/meridian.space/https/http-path/', 'https://meridian.space'], + ['/dns/meridian.space/https/http-path', 'https://meridian.space'] ] for (const [multiaddr, expectedUri] of HAPPY_CASES) { @@ -22,7 +25,10 @@ for (const [multiaddr, expectedUri] of HAPPY_CASES) { const ERROR_CASES = [ ['/ip4/127.0.0.1/tcp/80', 'Cannot parse "/ip4/127.0.0.1/tcp/80": unsupported scheme "undefined"'], ['/ip4/127.0.0.1/udp/90', 'Cannot parse "/ip4/127.0.0.1/udp/90": unsupported protocol "udp"'], - ['/ip4/127.0.0.1/tcp/8080/http/p2p/pubkey', 'Cannot parse "/ip4/127.0.0.1/tcp/8080/http/p2p/pubkey": too many parts'] + ['/ip4/127.0.0.1/tcp/8080/http/p2p/pubkey', 'Cannot parse "/ip4/127.0.0.1/tcp/8080/http/p2p/pubkey": too many parts'], + // NOTE: This is a valid multiaddr value that we decided to not support yet. + ['/dns/meridian.space/tcp/8080/http/http-path/%2Fipni-provider%2FproviderID', 'Cannot parse "/dns/meridian.space/tcp/8080/http/http-path/%2Fipni-provider%2FproviderID": unsupported scheme "tcp"'], + ['/dns/meridian.space/http/http-path/invalid%path', 'Cannot parse "/dns/meridian.space/http/http-path/invalid%path": unsupported http path'] ] for (const [multiaddr, expectedError] of ERROR_CASES) { diff --git a/test/spark.js b/test/spark.js index 58c5fe8..a240e89 100644 --- a/test/spark.js +++ b/test/spark.js @@ -205,13 +205,20 @@ test('fetchCAR fails with statusCode=702 (protocol is not tcp)', async () => { assertEquals(stats.statusCode, 702, 'stats.statusCode') }) -test('fetchCAR fails with statusCode=703 (scheme is not http/https)', async () => { +test('fetchCAR fails with statusCode=703 (scheme is not http/https) - multiaddr without http-path', async () => { const spark = new Spark() const stats = newStats() await spark.fetchCAR('http', '/ip4/1.2.3.4/tcp/80/ldap', KNOWN_CID, stats) assertEquals(stats.statusCode, 703, 'stats.statusCode') }) +test('fetchCAR fails with statusCode=703 (scheme is not supported) - multiaddr with http-path', async () => { + const spark = new Spark() + const stats = newStats() + await spark.fetchCAR('http', '/dns/meridian.space/tcp/8080/http/http-path/%2Fipni-provider%2FproviderID', KNOWN_CID, stats) + assertEquals(stats.statusCode, 703, 'stats.statusCode') +}) + test('fetchCAR fails with statusCode=704 (multiaddr has too many parts)', async () => { const spark = new Spark() const stats = newStats() @@ -219,6 +226,13 @@ test('fetchCAR fails with statusCode=704 (multiaddr has too many parts)', async assertEquals(stats.statusCode, 704, 'stats.statusCode') }) +test('fetchCAR fails with statusCode=705 (multiaddr has invalid http-path)', async () => { + const spark = new Spark() + const stats = newStats() + await spark.fetchCAR('http', '/dns/meridian.space/http/http-path/invalid%path', KNOWN_CID, stats) + assertEquals(stats.statusCode, 705, 'stats.statusCode') +}) + test('fetchCAR fails with statusCode=801 (DNS error)', async () => { const spark = new Spark() const stats = newStats()