diff --git a/README.md b/README.md index 06f521f..8a6d9c5 100644 --- a/README.md +++ b/README.md @@ -35,40 +35,63 @@ > npm i libp2p-websockets ``` -### Example +### Constructor properties ```js const WS = require('libp2p-websockets') -const multiaddr = require('multiaddr') -const pipe = require('it-pipe') -const { collect } = require('streaming-iterables') -const addr = multiaddr('/ip4/0.0.0.0/tcp/9090/ws') +const properties = { + upgrader, + filter +} -const ws = new WS({ upgrader }) +const ws = new WS(properties) +``` -const listener = ws.createListener((socket) => { - console.log('new connection opened') - pipe( - ['hello'], - socket - ) -}) +| Name | Type | Description | Default | +|------|------|-------------|---------| +| upgrader | [`Upgrader`](https://github.com/libp2p/interface-transport#upgrader) | connection upgrader object with `upgradeOutbound` and `upgradeInbound` | **REQUIRED** | +| filter | `(multiaddrs: Array) => Array` | override transport addresses filter | **Browser:** DNS+WSS multiaddrs / **Node.js:** DNS+{WS, WSS} multiaddrs | + +You can create your own address filters for this transports, or rely in the filters [provided](./src/filters.js). + +The available filters are: -await listener.listen(addr) -console.log('listening') +- `filters.all` + - Returns all TCP and DNS based addresses, both with `ws` or `wss`. +- `filters.dnsWss` + - Returns all DNS based addresses with `wss`. +- `filters.dnsWsOrWss` + - Returns all DNS based addresses, both with `ws` or `wss`. -const socket = await ws.dial(addr) -const values = await pipe( - socket, - collect -) -console.log(`Value: ${values.toString()}`) +## Libp2p Usage Example -// Close connection after reading -await listener.close() +```js +const Libp2p = require('libp2p') +const Websockets = require('libp2p-websockets') +const filters = require('libp2p-websockets/src/filters') +const MPLEX = require('libp2p-mplex') +const { NOISE } = require('libp2p-noise') + +const transportKey = Websockets.prototype[Symbol.toStringTag] +const node = await Libp2p.create({ + modules: { + transport: [Websockets], + streamMuxer: [MPLEX], + connEncryption: [NOISE] + }, + config: { + transport: { + [transportKey]: { // Transport properties -- Libp2p upgrader is automatically added + filter: filters.dnsWsOrWss + } + } + } +}) ``` +For more information see [libp2p/js-libp2p/doc/CONFIGURATION.md#customizing-transports](https://github.com/libp2p/js-libp2p/blob/master/doc/CONFIGURATION.md#customizing-transports). + ## API ### Transport diff --git a/package.json b/package.json index 2b38e64..fd9d78c 100644 --- a/package.json +++ b/package.json @@ -40,22 +40,24 @@ "dependencies": { "abortable-iterator": "^3.0.0", "class-is": "^1.1.0", - "debug": "^4.1.1", - "err-code": "^2.0.0", - "it-ws": "^3.0.0", - "libp2p-utils": "^0.2.0", - "mafmt": "^8.0.0", - "multiaddr": "^8.0.0", + "debug": "^4.2.0", + "err-code": "^2.0.3", + "ipfs-utils": "^4.0.1", + "it-ws": "^3.0.2", + "libp2p-utils": "^0.2.1", + "mafmt": "^8.0.1", + "multiaddr": "^8.1.1", "multiaddr-to-uri": "^6.0.0", "p-timeout": "^3.2.0" }, "devDependencies": { "abort-controller": "^3.0.0", - "aegir": "^25.0.0", + "aegir": "^28.1.0", "bl": "^4.0.0", + "is-loopback-addr": "^1.0.1", "it-goodbye": "^2.0.1", "it-pipe": "^1.0.1", - "libp2p-interfaces": "^0.4.0", + "libp2p-interfaces": "^0.7.1", "streaming-iterables": "^5.0.2", "uint8arrays": "^1.1.0" }, diff --git a/src/constants.js b/src/constants.js index b7ab8fe..28f2634 100644 --- a/src/constants.js +++ b/src/constants.js @@ -4,5 +4,9 @@ exports.CODE_P2P = 421 exports.CODE_CIRCUIT = 290 +exports.CODE_TCP = 6 +exports.CODE_WS = 477 +exports.CODE_WSS = 478 + // Time to wait for a connection to close gracefully before destroying it manually exports.CLOSE_TIMEOUT = 2000 diff --git a/src/filters.js b/src/filters.js new file mode 100644 index 0000000..2aa0bc8 --- /dev/null +++ b/src/filters.js @@ -0,0 +1,49 @@ +'use strict' + +const mafmt = require('mafmt') +const { + CODE_CIRCUIT, + CODE_P2P, + CODE_TCP, + CODE_WS, + CODE_WSS +} = require('./constants') + +module.exports = { + all: (multiaddrs) => multiaddrs.filter((ma) => { + if (ma.protoCodes().includes(CODE_CIRCUIT)) { + return false + } + + const testMa = ma.decapsulateCode(CODE_P2P) + + return mafmt.WebSockets.matches(testMa) || + mafmt.WebSocketsSecure.matches(testMa) + }), + dnsWss: (multiaddrs) => multiaddrs.filter((ma) => { + if (ma.protoCodes().includes(CODE_CIRCUIT)) { + return false + } + + const testMa = ma.decapsulateCode(CODE_P2P) + + return mafmt.WebSocketsSecure.matches(testMa) && + mafmt.DNS.matches(testMa.decapsulateCode(CODE_TCP).decapsulateCode(CODE_WSS)) + }), + dnsWsOrWss: (multiaddrs) => multiaddrs.filter((ma) => { + if (ma.protoCodes().includes(CODE_CIRCUIT)) { + return false + } + + const testMa = ma.decapsulateCode(CODE_P2P) + + // WS + if (mafmt.WebSockets.matches(testMa)) { + return mafmt.DNS.matches(testMa.decapsulateCode(CODE_TCP).decapsulateCode(CODE_WS)) + } + + // WSS + return mafmt.WebSocketsSecure.matches(testMa) && + mafmt.DNS.matches(testMa.decapsulateCode(CODE_TCP).decapsulateCode(CODE_WSS)) + }) +} diff --git a/src/index.js b/src/index.js index d22bdca..3d985ae 100644 --- a/src/index.js +++ b/src/index.js @@ -1,38 +1,40 @@ 'use strict' const connect = require('it-ws/client') -const mafmt = require('mafmt') const withIs = require('class-is') const toUri = require('multiaddr-to-uri') const { AbortError } = require('abortable-iterator') const log = require('debug')('libp2p:websockets') +const env = require('ipfs-utils/src/env') const createListener = require('./listener') const toConnection = require('./socket-to-conn') -const { CODE_CIRCUIT, CODE_P2P } = require('./constants') +const filters = require('./filters') /** * @class WebSockets */ class WebSockets { /** - * @constructor + * @class * @param {object} options * @param {Upgrader} options.upgrader + * @param {(multiaddrs: Array) => Array} options.filter - override transport addresses filter */ - constructor ({ upgrader }) { + constructor ({ upgrader, filter }) { if (!upgrader) { throw new Error('An upgrader must be provided. See https://github.com/libp2p/interface-transport#upgrader.') } this._upgrader = upgrader + this._filter = filter } /** * @async * @param {Multiaddr} ma * @param {object} [options] - * @param {AbortSignal} [options.signal] Used to abort dial requests + * @param {AbortSignal} [options.signal] - Used to abort dial requests * @returns {Connection} An upgraded Connection */ async dial (ma, options = {}) { @@ -51,7 +53,7 @@ class WebSockets { * @private * @param {Multiaddr} ma * @param {object} [options] - * @param {AbortSignal} [options.signal] Used to abort dial requests + * @param {AbortSignal} [options.signal] - Used to abort dial requests * @returns {Promise} Resolves a extended duplex iterable on top of a WebSocket */ async _connect (ma, options = {}) { @@ -97,8 +99,9 @@ class WebSockets { * Creates a Websockets listener. The provided `handler` function will be called * anytime a new incoming Connection has been successfully upgraded via * `upgrader.upgradeInbound`. + * * @param {object} [options] - * @param {http.Server} [options.server] A pre-created Node.js HTTP/S server. + * @param {http.Server} [options.server] - A pre-created Node.js HTTP/S server. * @param {function (Connection)} handler * @returns {Listener} A Websockets listener */ @@ -112,21 +115,26 @@ class WebSockets { } /** - * Takes a list of `Multiaddr`s and returns only valid Websockets addresses + * Takes a list of `Multiaddr`s and returns only valid Websockets addresses. + * By default, in a browser environment only DNS+WSS multiaddr is accepted, + * while in a Node.js environment DNS+{WS, WSS} multiaddrs are accepted. + * * @param {Multiaddr[]} multiaddrs * @returns {Multiaddr[]} Valid Websockets multiaddrs */ filter (multiaddrs) { multiaddrs = Array.isArray(multiaddrs) ? multiaddrs : [multiaddrs] - return multiaddrs.filter((ma) => { - if (ma.protoCodes().includes(CODE_CIRCUIT)) { - return false - } + if (this._filter) { + return this._filter(multiaddrs) + } - return mafmt.WebSockets.matches(ma.decapsulateCode(CODE_P2P)) || - mafmt.WebSocketsSecure.matches(ma.decapsulateCode(CODE_P2P)) - }) + // Browser + if (env.isBrowser || env.isWebWorker) { + return filters.dnsWss(multiaddrs) + } + + return filters.all(multiaddrs) } } diff --git a/test/browser.js b/test/browser.js index 261bbea..092646e 100644 --- a/test/browser.js +++ b/test/browser.js @@ -34,6 +34,16 @@ describe('libp2p-websockets', () => { expect(results).to.eql([message]) }) + it('should filter out no DNS websocket addresses', function () { + const ma1 = multiaddr('/ip4/127.0.0.1/tcp/80/ws') + const ma2 = multiaddr('/ip4/127.0.0.1/tcp/443/wss') + const ma3 = multiaddr('/ip6/::1/tcp/80/ws') + const ma4 = multiaddr('/ip6/::1/tcp/443/wss') + + const valid = ws.filter([ma1, ma2, ma3, ma4]) + expect(valid.length).to.equal(0) + }) + describe('stress', () => { it('one big write', async () => { const rawMessage = new Uint8Array(1000000).fill('a') diff --git a/test/compliance.node.js b/test/compliance.node.js index 43e83e9..ece8c7f 100644 --- a/test/compliance.node.js +++ b/test/compliance.node.js @@ -5,11 +5,12 @@ const tests = require('libp2p-interfaces/src/transport/tests') const multiaddr = require('multiaddr') const http = require('http') const WS = require('../src') +const filters = require('../src/filters') describe('interface-transport compliance', () => { tests({ async setup ({ upgrader }) { // eslint-disable-line require-await - const ws = new WS({ upgrader }) + const ws = new WS({ upgrader, filter: filters.all }) const addrs = [ multiaddr('/ip4/127.0.0.1/tcp/9091/ws'), multiaddr('/ip4/127.0.0.1/tcp/9092/ws'), diff --git a/test/node.js b/test/node.js index ad51a99..83bcd8f 100644 --- a/test/node.js +++ b/test/node.js @@ -8,12 +8,14 @@ const fs = require('fs') const { expect } = require('aegir/utils/chai') const multiaddr = require('multiaddr') const goodbye = require('it-goodbye') +const isLoopbackAddr = require('is-loopback-addr') const { collect } = require('streaming-iterables') const pipe = require('it-pipe') const BufferList = require('bl/BufferList') const uint8ArrayFromString = require('uint8arrays/from-string') const WS = require('../src') +const filters = require('../src/filters') require('./compliance.node') @@ -250,6 +252,36 @@ describe('dial', () => { }) }) + describe('ip4 no loopback', () => { + let ws + let listener + const ma = multiaddr('/ip4/0.0.0.0/tcp/0/ws') + + beforeEach(() => { + ws = new WS({ upgrader: mockUpgrader }) + listener = ws.createListener(conn => pipe(conn, conn)) + return listener.listen(ma) + }) + + afterEach(() => listener.close()) + + it('dial', async () => { + const addrs = listener.getAddrs().filter((ma) => { + const { address } = ma.nodeAddress() + + return !isLoopbackAddr(address) + }) + + // Dial first no loopback address + const conn = await ws.dial(addrs[0]) + const s = goodbye({ source: ['hey'], sink: collect }) + + const result = await pipe(s, conn, s) + + expect(result).to.be.eql([uint8ArrayFromString('hey')]) + }) + }) + describe('ip4 with wss', () => { let ws let listener @@ -327,11 +359,79 @@ describe('dial', () => { describe('filter addrs', () => { let ws - before(() => { - ws = new WS({ upgrader: mockUpgrader }) + describe('default filter addrs with only dns', () => { + before(() => { + ws = new WS({ upgrader: mockUpgrader }) + }) + + it('should filter out invalid WS addresses', function () { + const ma1 = multiaddr('/ip4/127.0.0.1/tcp/9090') + const ma2 = multiaddr('/ip4/127.0.0.1/udp/9090') + const ma3 = multiaddr('/ip6/::1/tcp/80') + const ma4 = multiaddr('/dnsaddr/ipfs.io/tcp/80') + + const valid = ws.filter([ma1, ma2, ma3, ma4]) + expect(valid.length).to.equal(0) + }) + + it('should filter correct dns address', function () { + const ma1 = multiaddr('/dnsaddr/ipfs.io/ws') + const ma2 = multiaddr('/dnsaddr/ipfs.io/tcp/80/ws') + const ma3 = multiaddr('/dnsaddr/ipfs.io/tcp/80/wss') + + const valid = ws.filter([ma1, ma2, ma3]) + expect(valid.length).to.equal(3) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + expect(valid[2]).to.deep.equal(ma3) + }) + + it('should filter correct dns address with ipfs id', function () { + const ma1 = multiaddr('/dnsaddr/ipfs.io/tcp/80/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const ma2 = multiaddr('/dnsaddr/ipfs.io/tcp/443/wss/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + + const valid = ws.filter([ma1, ma2]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + }) + + it('should filter correct dns4 address', function () { + const ma1 = multiaddr('/dns4/ipfs.io/tcp/80/ws') + const ma2 = multiaddr('/dns4/ipfs.io/tcp/443/wss') + + const valid = ws.filter([ma1, ma2]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + }) + + it('should filter correct dns6 address', function () { + const ma1 = multiaddr('/dns6/ipfs.io/tcp/80/ws') + const ma2 = multiaddr('/dns6/ipfs.io/tcp/443/wss') + + const valid = ws.filter([ma1, ma2]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + }) + + it('should filter correct dns6 address with ipfs id', function () { + const ma1 = multiaddr('/dns6/ipfs.io/tcp/80/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const ma2 = multiaddr('/dns6/ipfs.io/tcp/443/wss/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + + const valid = ws.filter([ma1, ma2]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + }) }) - describe('filter valid addrs for this transport', function () { + describe('custom filter addrs', () => { + before(() => { + ws = new WS({ upgrader: mockUpgrader, filter: filters.all }) + }) + it('should fail invalid WS addresses', function () { const ma1 = multiaddr('/ip4/127.0.0.1/tcp/9090') const ma2 = multiaddr('/ip4/127.0.0.1/udp/9090') @@ -447,14 +547,13 @@ describe('filter addrs', () => { expect(valid[0]).to.deep.equal(ma1) expect(valid[1]).to.deep.equal(ma4) }) - }) - it('filter a single addr for this transport', (done) => { - const ma = multiaddr('/ip4/127.0.0.1/tcp/9090/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + it('filter a single addr for this transport', () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/9090/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') - const valid = ws.filter(ma) - expect(valid.length).to.equal(1) - expect(valid[0]).to.deep.equal(ma) - done() + const valid = ws.filter(ma) + expect(valid.length).to.equal(1) + expect(valid[0]).to.deep.equal(ma) + }) }) })