feat: custom address filter (#116)

* feat: custom address filter

BREAKING CHANGE: Only DNS+WSS addresses are now returned on filter by default in the browser. This can be overritten by the filter option and filters are provided in the module.
This commit is contained in:
Vasco Santos 2020-11-24 10:14:01 +01:00 committed by GitHub
parent 662d04128c
commit 711c721b03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 257 additions and 61 deletions

View File

@ -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 listener = ws.createListener((socket) => {
console.log('new connection opened')
pipe(
['hello'],
socket
)
})
await listener.listen(addr)
console.log('listening')
const socket = await ws.dial(addr)
const values = await pipe(
socket,
collect
)
console.log(`Value: ${values.toString()}`)
// Close connection after reading
await listener.close()
const ws = new WS(properties)
```
| Name | Type | Description | Default |
|------|------|-------------|---------|
| upgrader | [`Upgrader`](https://github.com/libp2p/interface-transport#upgrader) | connection upgrader object with `upgradeOutbound` and `upgradeInbound` | **REQUIRED** |
| filter | `(multiaddrs: Array<Multiaddr>) => Array<Multiaddr>` | 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:
- `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`.
## Libp2p Usage Example
```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

View File

@ -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"
},

View File

@ -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

49
src/filters.js Normal file
View File

@ -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))
})
}

View File

@ -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<Multiaddr>) => Array<Multiaddr>} 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<WebSocket>} 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)
}
}

View File

@ -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')

View File

@ -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'),

View File

@ -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
describe('default filter addrs with only dns', () => {
before(() => {
ws = new WS({ upgrader: mockUpgrader })
})
describe('filter valid addrs for this transport', function () {
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('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) => {
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()
})
})
})