mirror of
https://github.com/fluencelabs/js-libp2p
synced 2025-07-11 23:01:34 +00:00
Compare commits
122 Commits
docs/deleg
...
v0.27.0-pr
Author | SHA1 | Date | |
---|---|---|---|
92ed56657c | |||
9900beb243 | |||
4a871bbf8b | |||
a39889c4ea | |||
9bbe93c772 | |||
cc65a4b06f | |||
9c884a72b0 | |||
3ee1e22242 | |||
45f47023d2 | |||
af96dcc499 | |||
f540112835 | |||
3d30cb18cd | |||
64cbf90e02 | |||
7fc1900343 | |||
ad15d4ed09 | |||
600f761009 | |||
a2f31d99d2 | |||
edaa67dfd0 | |||
9b10e09cc0 | |||
8c6ad79630 | |||
1838a641d9 | |||
3cadeb39cb | |||
43440aa8a6 | |||
7c3371bf17 | |||
43b98e64b6 | |||
962081f448 | |||
754fbc2d0b | |||
0a8f9f3238 | |||
3b52236dee | |||
c7dcfe5e48 | |||
3b06283ad8 | |||
74bfe6bea5 | |||
53ce404260 | |||
43a3b85f1a | |||
e8bf12b68a | |||
7d1cb5423f | |||
c4be5f4aaf | |||
a37c5c0144 | |||
24c603741f | |||
ea62c52701 | |||
f9fe44f6b7 | |||
d5405dbb08 | |||
571fd3b7d1 | |||
cba2c6d8b2 | |||
f8540fa3ed | |||
0cacfe29a5 | |||
c4bc00be9c | |||
f3eb1f1201 | |||
dbb9e57311 | |||
11ed6bd14c | |||
fc22c36ba7 | |||
b518391a47 | |||
997ee166b0 | |||
acbbc0f84e | |||
995640ee2f | |||
b316cdd19b | |||
1ea945ad24 | |||
c37703dc17 | |||
86b275a0d3 | |||
3c79d33db9 | |||
34d57f8989 | |||
ced2dbf318 | |||
44d47087d1 | |||
797d8f0cf1 | |||
f3e276eb79 | |||
138bb0bbae | |||
af364b070b | |||
10c8553c58 | |||
a7d5e67e06 | |||
4f8043d259 | |||
b277b26043 | |||
bc071ce7d7 | |||
01730214d6 | |||
0826531e31 | |||
35ac02dcb5 | |||
b73348078d | |||
21cd9c67bc | |||
0fc4537a5e | |||
6a05f3e6e7 | |||
93a1e42ef3 | |||
35aa45ce92 | |||
b11c6fc7e9 | |||
ebedd3510b | |||
ae6af20e8e | |||
2a80618740 | |||
5b1bd389f8 | |||
3e31c2d0df | |||
8079c2078b | |||
80cf0777b5 | |||
60b0cbc179 | |||
3eef695bc0 | |||
b3deb356f1 | |||
299cfefa01 | |||
aa95ab9928 | |||
b0f124b5ff | |||
b294301456 | |||
d92306f222 | |||
fd738f9d51 | |||
d788433b43 | |||
d5a977b227 | |||
0489972b4b | |||
3f31b1f422 | |||
a2b3446ed7 | |||
ff7a6c86a0 | |||
9a8d609a59 | |||
9fef58cb7d | |||
684f283aec | |||
3e95e6f9e4 | |||
f4f3f0f03a | |||
7c2c852fc0 | |||
e8d8aab278 | |||
dd48d268ec | |||
99a53592e2 | |||
2a2e7a1012 | |||
791f39a09b | |||
65d52857a5 | |||
48b1b442e9 | |||
9554b05c6f | |||
df6ef45a2d | |||
b4a70ea476 | |||
45716da465 | |||
905c911946 |
105
.aegir.js
105
.aegir.js
@ -1,82 +1,47 @@
|
||||
'use strict'
|
||||
|
||||
const pull = require('pull-stream')
|
||||
const parallel = require('async/parallel')
|
||||
const WebSocketStarRendezvous = require('libp2p-websocket-star-rendezvous')
|
||||
const sigServer = require('libp2p-webrtc-star/src/sig-server')
|
||||
const Libp2p = require('./src')
|
||||
const { MULTIADDRS_WEBSOCKETS } = require('./test/fixtures/browser')
|
||||
const Peers = require('./test/fixtures/peers')
|
||||
const PeerId = require('peer-id')
|
||||
const PeerInfo = require('peer-info')
|
||||
const WebSockets = require('libp2p-websockets')
|
||||
const Muxer = require('libp2p-mplex')
|
||||
const Crypto = require('libp2p-secio')
|
||||
const pipe = require('it-pipe')
|
||||
let libp2p
|
||||
|
||||
const Node = require('./test/utils/bundle-nodejs.js')
|
||||
const {
|
||||
getPeerRelay,
|
||||
WRTC_RENDEZVOUS_MULTIADDR,
|
||||
WS_RENDEZVOUS_MULTIADDR
|
||||
} = require('./test/utils/constants')
|
||||
const before = async () => {
|
||||
// Use the last peer
|
||||
const peerId = await PeerId.createFromJSON(Peers[Peers.length - 1])
|
||||
const peerInfo = new PeerInfo(peerId)
|
||||
peerInfo.multiaddrs.add(MULTIADDRS_WEBSOCKETS[0])
|
||||
|
||||
let wrtcRendezvous
|
||||
let wsRendezvous
|
||||
let node
|
||||
|
||||
const before = (done) => {
|
||||
parallel([
|
||||
(cb) => {
|
||||
sigServer.start({
|
||||
port: WRTC_RENDEZVOUS_MULTIADDR.nodeAddress().port
|
||||
// cryptoChallenge: true TODO: needs https://github.com/libp2p/js-libp2p-webrtc-star/issues/128
|
||||
})
|
||||
.then(server => {
|
||||
wrtcRendezvous = server
|
||||
cb()
|
||||
})
|
||||
.catch(cb)
|
||||
libp2p = new Libp2p({
|
||||
peerInfo,
|
||||
modules: {
|
||||
transport: [WebSockets],
|
||||
streamMuxer: [Muxer],
|
||||
connEncryption: [Crypto]
|
||||
},
|
||||
(cb) => {
|
||||
WebSocketStarRendezvous.start({
|
||||
port: WS_RENDEZVOUS_MULTIADDR.nodeAddress().port,
|
||||
refreshPeerListIntervalMS: 1000,
|
||||
strictMultiaddr: false,
|
||||
cryptoChallenge: true
|
||||
}, (err, _server) => {
|
||||
if (err) {
|
||||
return cb(err)
|
||||
config: {
|
||||
relay: {
|
||||
enabled: true,
|
||||
hop: {
|
||||
enabled: true,
|
||||
active: false
|
||||
}
|
||||
wsRendezvous = _server
|
||||
cb()
|
||||
})
|
||||
},
|
||||
(cb) => {
|
||||
getPeerRelay((err, peerInfo) => {
|
||||
if (err) {
|
||||
return done(err)
|
||||
}
|
||||
|
||||
node = new Node({
|
||||
peerInfo,
|
||||
config: {
|
||||
relay: {
|
||||
enabled: true,
|
||||
hop: {
|
||||
enabled: true,
|
||||
active: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
node.handle('/echo/1.0.0', (protocol, conn) => pull(conn, conn))
|
||||
node.start(cb)
|
||||
})
|
||||
}
|
||||
}
|
||||
], done)
|
||||
})
|
||||
// Add the echo protocol
|
||||
libp2p.handle('/echo/1.0.0', ({ stream }) => pipe(stream, stream))
|
||||
|
||||
await libp2p.start()
|
||||
}
|
||||
|
||||
const after = (done) => {
|
||||
setTimeout(() =>
|
||||
parallel([
|
||||
(cb) => wrtcRendezvous.stop().then(cb).catch(cb),
|
||||
...[node, wsRendezvous].map((s) => (cb) => s.stop(cb)),
|
||||
], done),
|
||||
2000
|
||||
)
|
||||
const after = async () => {
|
||||
await libp2p.stop()
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -9,6 +9,7 @@ logs
|
||||
*.log
|
||||
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
|
@ -21,7 +21,8 @@ jobs:
|
||||
- stage: check
|
||||
script:
|
||||
- npx aegir build --bundlesize
|
||||
- npx aegir dep-check -- -i wrtc -i electron-webrtc
|
||||
# Remove pull libs once ping is async
|
||||
- npx aegir dep-check -- -i pull-handshake -i pull-stream
|
||||
- npm run lint
|
||||
|
||||
- stage: test
|
||||
@ -29,16 +30,14 @@ jobs:
|
||||
addons:
|
||||
chrome: stable
|
||||
script:
|
||||
- npx aegir test -t browser
|
||||
- npx aegir test -t webworker
|
||||
- npx aegir test -t browser -t webworker
|
||||
|
||||
- stage: test
|
||||
name: firefox
|
||||
addons:
|
||||
firefox: latest
|
||||
script:
|
||||
- npx aegir test -t browser -- --browsers FirefoxHeadless
|
||||
- npx aegir test -t webworker -- --browsers FirefoxHeadless
|
||||
- npx aegir test -t browser -t webworker -- --browsers FirefoxHeadless
|
||||
|
||||
notifications:
|
||||
email: false
|
143
CHANGELOG.md
143
CHANGELOG.md
@ -1,3 +1,146 @@
|
||||
<a name="0.27.0-pre.1"></a>
|
||||
# [0.27.0-pre.1](https://github.com/libp2p/js-libp2p/compare/v0.27.0-pre.0...v0.27.0-pre.1) (2019-12-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* coalescing dial support ([#518](https://github.com/libp2p/js-libp2p/issues/518)) ([4a871bb](https://github.com/libp2p/js-libp2p/commit/4a871bb))
|
||||
|
||||
|
||||
|
||||
<a name="0.27.0-pre.0"></a>
|
||||
# [0.27.0-pre.0](https://github.com/libp2p/js-libp2p/compare/v0.26.2...v0.27.0-pre.0) (2019-12-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* clean up peer discovery flow ([#494](https://github.com/libp2p/js-libp2p/issues/494)) ([f3eb1f1](https://github.com/libp2p/js-libp2p/commit/f3eb1f1))
|
||||
* clean up pending dials abort per feedback ([7c3371b](https://github.com/libp2p/js-libp2p/commit/7c3371b))
|
||||
* correct release readme ([c4bc00b](https://github.com/libp2p/js-libp2p/commit/c4bc00b))
|
||||
* examples readme typos ([#481](https://github.com/libp2p/js-libp2p/issues/481)) ([35ac02d](https://github.com/libp2p/js-libp2p/commit/35ac02d))
|
||||
* performance bottleneck in stat.js ([#463](https://github.com/libp2p/js-libp2p/issues/463)) ([93a1e42](https://github.com/libp2p/js-libp2p/commit/93a1e42))
|
||||
* release tokens as soon as they are available ([43440aa](https://github.com/libp2p/js-libp2p/commit/43440aa))
|
||||
* replace peerInfo addresses with listen addresses ([#485](https://github.com/libp2p/js-libp2p/issues/485)) ([acbbc0f](https://github.com/libp2p/js-libp2p/commit/acbbc0f))
|
||||
* token release logic ([1838a64](https://github.com/libp2p/js-libp2p/commit/1838a64))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* abort all pending dials on stop ([754fbc2](https://github.com/libp2p/js-libp2p/commit/754fbc2))
|
||||
* add early token recycling in ([24c6037](https://github.com/libp2p/js-libp2p/commit/24c6037))
|
||||
* add token based dialer ([f8540fa](https://github.com/libp2p/js-libp2p/commit/f8540fa))
|
||||
* discovery modules ([#486](https://github.com/libp2p/js-libp2p/issues/486)) ([997ee16](https://github.com/libp2p/js-libp2p/commit/997ee16))
|
||||
* discovery modules from transports should be added ([#510](https://github.com/libp2p/js-libp2p/issues/510)) ([af96dcc](https://github.com/libp2p/js-libp2p/commit/af96dcc))
|
||||
* peer store ([#470](https://github.com/libp2p/js-libp2p/issues/470)) ([f3e276e](https://github.com/libp2p/js-libp2p/commit/f3e276e))
|
||||
* registrar ([#471](https://github.com/libp2p/js-libp2p/issues/471)) ([797d8f0](https://github.com/libp2p/js-libp2p/commit/797d8f0))
|
||||
* support peer-id instances in peer store operations ([#491](https://github.com/libp2p/js-libp2p/issues/491)) ([11ed6bd](https://github.com/libp2p/js-libp2p/commit/11ed6bd))
|
||||
|
||||
|
||||
|
||||
<a name="0.26.2"></a>
|
||||
## [0.26.2](https://github.com/libp2p/js-libp2p/compare/v0.26.1...v0.26.2) (2019-09-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* pubsub promisify ([#456](https://github.com/libp2p/js-libp2p/issues/456)) ([ae6af20](https://github.com/libp2p/js-libp2p/commit/ae6af20))
|
||||
|
||||
|
||||
|
||||
<a name="0.26.1"></a>
|
||||
## [0.26.1](https://github.com/libp2p/js-libp2p/compare/v0.26.0...v0.26.1) (2019-08-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* avoid using superstruct interface ([aa95ab9](https://github.com/libp2p/js-libp2p/commit/aa95ab9))
|
||||
* improve config defaults ([#409](https://github.com/libp2p/js-libp2p/issues/409)) ([3eef695](https://github.com/libp2p/js-libp2p/commit/3eef695)), closes [#406](https://github.com/libp2p/js-libp2p/issues/406)
|
||||
* pubsub configuration ([#404](https://github.com/libp2p/js-libp2p/issues/404)) ([b0f124b](https://github.com/libp2p/js-libp2p/commit/b0f124b)), closes [#401](https://github.com/libp2p/js-libp2p/issues/401) [#401](https://github.com/libp2p/js-libp2p/issues/401) [#401](https://github.com/libp2p/js-libp2p/issues/401) [#401](https://github.com/libp2p/js-libp2p/issues/401) [#401](https://github.com/libp2p/js-libp2p/issues/401)
|
||||
* reference files directly to avoid npm install failures ([#408](https://github.com/libp2p/js-libp2p/issues/408)) ([b3deb35](https://github.com/libp2p/js-libp2p/commit/b3deb35))
|
||||
* reject rather than throw in get peer info ([#410](https://github.com/libp2p/js-libp2p/issues/410)) ([60b0cbc](https://github.com/libp2p/js-libp2p/commit/60b0cbc)), closes [#400](https://github.com/libp2p/js-libp2p/issues/400)
|
||||
|
||||
|
||||
|
||||
<a name="0.26.0"></a>
|
||||
# [0.26.0](https://github.com/libp2p/js-libp2p/compare/v0.26.0-rc.3...v0.26.0) (2019-08-07)
|
||||
|
||||
|
||||
|
||||
<a name="0.26.0-rc.3"></a>
|
||||
# [0.26.0-rc.3](https://github.com/libp2p/js-libp2p/compare/v0.26.0-rc.2...v0.26.0-rc.3) (2019-08-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* promisified methods ([#398](https://github.com/libp2p/js-libp2p/issues/398)) ([ff7a6c8](https://github.com/libp2p/js-libp2p/commit/ff7a6c8))
|
||||
|
||||
|
||||
|
||||
<a name="0.26.0-rc.2"></a>
|
||||
# [0.26.0-rc.2](https://github.com/libp2p/js-libp2p/compare/v0.26.0-rc.1...v0.26.0-rc.2) (2019-08-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* dont override methods of created instance ([#394](https://github.com/libp2p/js-libp2p/issues/394)) ([3e95e6f](https://github.com/libp2p/js-libp2p/commit/3e95e6f))
|
||||
* pubsub default config ([#393](https://github.com/libp2p/js-libp2p/issues/393)) ([f4f3f0f](https://github.com/libp2p/js-libp2p/commit/f4f3f0f))
|
||||
|
||||
|
||||
### Chores
|
||||
|
||||
* update switch ([#395](https://github.com/libp2p/js-libp2p/issues/395)) ([684f283](https://github.com/libp2p/js-libp2p/commit/684f283))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* switch configuration has changed.
|
||||
'blacklistTTL' is now 'denyTTL' and 'blackListAttempts' is now 'denyAttempts'
|
||||
|
||||
|
||||
|
||||
<a name="0.26.0-rc.1"></a>
|
||||
# [0.26.0-rc.1](https://github.com/libp2p/js-libp2p/compare/v0.26.0-rc.0...v0.26.0-rc.1) (2019-07-31)
|
||||
|
||||
|
||||
|
||||
<a name="0.26.0-rc.0"></a>
|
||||
# [0.26.0-rc.0](https://github.com/libp2p/js-libp2p/compare/v0.25.5...v0.26.0-rc.0) (2019-07-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* make subscribe comply with ipfs interface ([#389](https://github.com/libp2p/js-libp2p/issues/389)) ([9554b05](https://github.com/libp2p/js-libp2p/commit/9554b05))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* integrate gossipsub by default ([#365](https://github.com/libp2p/js-libp2p/issues/365)) ([791f39a](https://github.com/libp2p/js-libp2p/commit/791f39a))
|
||||
* promisify all api methods that accept callbacks ([#381](https://github.com/libp2p/js-libp2p/issues/381)) ([df6ef45](https://github.com/libp2p/js-libp2p/commit/df6ef45))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* new configuration for deciding the implementation of pubsub to be used.
|
||||
In this context, the experimental flags were also removed. See the README for the latest usage.
|
||||
* The ipfs interface specified that options
|
||||
should be provided after the handler, not before.
|
||||
https://github.com/ipfs/interface-js-ipfs-core/blob/v0.109.0/SPEC/PUBSUB.md#pubsubsubscribe
|
||||
|
||||
This corrects the order of parameters. See the jsdocs examples
|
||||
for subscribe to see how it should be used.
|
||||
|
||||
|
||||
|
||||
<a name="0.25.5"></a>
|
||||
## [0.25.5](https://github.com/libp2p/js-libp2p/compare/v0.25.4...v0.25.5) (2019-07-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* peer routing for delegate router ([#377](https://github.com/libp2p/js-libp2p/issues/377)) ([905c911](https://github.com/libp2p/js-libp2p/commit/905c911)), closes [/github.com/libp2p/go-libp2p-core/blob/6e566d10f4a5447317a66d64c7459954b969bdab/routing/query.go#L15-L24](https://github.com//github.com/libp2p/go-libp2p-core/blob/6e566d10f4a5447317a66d64c7459954b969bdab/routing/query.go/issues/L15-L24)
|
||||
|
||||
|
||||
|
||||
<a name="0.25.4"></a>
|
||||
## [0.25.4](https://github.com/libp2p/js-libp2p/compare/v0.25.3...v0.25.4) (2019-06-07)
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
# Contributing guidelines
|
||||
|
||||
libp2p as a project, including js-libp2p and all of its modules, follows the [standard IPFS Community contributing guidelines](https://github.com/ipfs/community/blob/master/contribution-guidelines.md).
|
||||
libp2p as a project, including js-libp2p and all of its modules, follows the [standard IPFS Community contributing guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md).
|
||||
|
||||
We also adhere to the [IPFS JavaScript Community contributing guidelines](https://github.com/ipfs/community/blob/master/js-code-guidelines.md) which provide additional information of how to collaborate and contribute in the JavaScript implementation of libp2p.
|
||||
We also adhere to the [IPFS JavaScript Community contributing guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) which provide additional information of how to collaborate and contribute in the JavaScript implementation of libp2p.
|
||||
|
||||
We appreciate your time and attention for going over these. Please open an issue on [ipfs/community](https://github.com/ipfs/community) if you have any question.
|
||||
|
||||
|
564
README.md
564
README.md
@ -8,8 +8,9 @@
|
||||
<a href="http://ipn.io"><img src="https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square" /></a>
|
||||
<a href="http://libp2p.io/"><img src="https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square" /></a>
|
||||
<a href="http://webchat.freenode.net/?channels=%23libp2p"><img src="https://img.shields.io/badge/freenode-%23libp2p-yellow.svg?style=flat-square" /></a>
|
||||
<a href="https://riot.permaweb.io/#/room/#libp2p:permaweb.io"><img src="https://img.shields.io/badge/matrix-%23libp2p%3Apermaweb.io-blue.svg?style=flat-square" /> </a>
|
||||
<a href="https://discord.gg/66KBrm2"><img src="https://img.shields.io/discord/475789330380488707?color=blueviolet&label=discord&style=flat-square" /></a>
|
||||
<a href="https://discuss.libp2p.io"><img src="https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg" /></a>
|
||||
<a href="https://waffle.io/libp2p/libp2p"><img src="https://img.shields.io/badge/pm-waffle-yellow.svg?style=flat-square" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@ -29,9 +30,7 @@
|
||||
|
||||
We've come a long way, but this project is still in Alpha, lots of development is happening, API might change, beware of the Dragons 🐉..
|
||||
|
||||
**Want to get started?** Check our [examples folder](/examples). You can check the development status at the [Waffle Board](https://waffle.io/libp2p/js-libp2p).
|
||||
|
||||
[](https://waffle.io/libp2p/js-libp2p/metrics/throughput)
|
||||
**Want to get started?** Check our [examples folder](/examples).
|
||||
|
||||
[**`Weekly Core Dev Calls`**](https://github.com/ipfs/pm/issues/650)
|
||||
|
||||
@ -46,12 +45,11 @@ We've come a long way, but this project is still in Alpha, lots of development i
|
||||
## Table of Contents
|
||||
|
||||
- [Background](#background)
|
||||
- [Bundles](#bundles)
|
||||
- [Install](#install)
|
||||
- [Usage](#usage)
|
||||
- [Install](#install)
|
||||
- [Usage](#usage)
|
||||
- [Configuration](#configuration)
|
||||
- [API](#api)
|
||||
- [Events](#events)
|
||||
- [Tutorials and Examples](#tutorials-and-examples)
|
||||
- [Development](#development)
|
||||
- [Tests](#tests)
|
||||
- [Packages](#packages)
|
||||
@ -60,7 +58,7 @@ We've come a long way, but this project is still in Alpha, lots of development i
|
||||
|
||||
## Background
|
||||
|
||||
libp2p is the product of a long and arduous quest to understand the evolution of the Internet networking stack. In order to build P2P applications, devs have long had to made custom ad-hoc solutions to fit their needs, sometimes making some hard assumptions about their runtimes and the state of the network at the time of their development. Today, looking back more than 20 years, we see a clear pattern in the types of mechanisms built around the Internet Protocol, IP, which can be found throughout many layers of the OSI layer system, libp2p distils these mechanisms into flat categories and defines clear interfaces that once exposed, enable other protocols and applications to use and swap them, enabling upgradability and adaptability for the runtime, without breaking the API.
|
||||
libp2p is the product of a long and arduous quest to understand the evolution of the Internet networking stack. In order to build P2P applications, devs have long had to make custom ad-hoc solutions to fit their needs, sometimes making some hard assumptions about their runtimes and the state of the network at the time of their development. Today, looking back more than 20 years, we see a clear pattern in the types of mechanisms built around the Internet Protocol, IP, which can be found throughout many layers of the OSI layer system, libp2p distils these mechanisms into flat categories and defines clear interfaces that once exposed, enable other protocols and applications to use and swap them, enabling upgradability and adaptability for the runtime, without breaking the API.
|
||||
|
||||
We are in the process of writing better documentation, blog posts, tutorials and a formal specification. Today you can find:
|
||||
|
||||
@ -75,460 +73,33 @@ We are in the process of writing better documentation, blog posts, tutorials and
|
||||
|
||||
To sum up, libp2p is a "network stack" -- a protocol suite -- that cleanly separates concerns, and enables sophisticated applications to only use the protocols they absolutely need, without giving up interoperability and upgradeability. libp2p grew out of IPFS, but it is built so that lots of people can use it, for lots of different projects.
|
||||
|
||||
## Bundles
|
||||
|
||||
With its modular nature, libp2p can be found being used in different projects with different sets of features, while preserving the same top level API. `js-libp2p` is only a skeleton and should not be installed directly, if you are looking for a prebundled libp2p stack, please check:
|
||||
|
||||
- [libp2p-ipfs-nodejs](https://github.com/ipfs/js-ipfs/tree/master/src/core/runtime/libp2p-nodejs.js) - The libp2p build used by js-ipfs when run in Node.js
|
||||
- [libp2p-ipfs-browser](https://github.com/ipfs/js-ipfs/tree/master/src/core/runtime/libp2p-browser.js) - The libp2p build used by js-ipfs when run in a Browser (that supports WebRTC)
|
||||
|
||||
If you have developed a libp2p bundle, please consider submitting it to this list so that it can be found easily by the users of libp2p.
|
||||
|
||||
## Install
|
||||
|
||||
Again, as noted above, this module is only a skeleton and should not be used directly other than libp2p bundle implementors that want to extend its code.
|
||||
|
||||
```sh
|
||||
npm install --save libp2p
|
||||
npm install libp2p
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### [Tutorials and Examples](/examples)
|
||||
### Configuration
|
||||
|
||||
You can find multiple examples on the [examples folder](/examples) that will guide you through using libp2p for several scenarios.
|
||||
|
||||
### Creating your own libp2p bundle
|
||||
|
||||
The libp2p module acts as a glue for every libp2p module that you can use to create your own libp2p bundle. Creating your own libp2p bundle gives you a lot of freedom when it comes to customize it with features and default setup. We recommend creating your own libp2p bundle for the app you are developing that takes in account your needs (e.g. for a browser working version of libp2p that acts as the network layer of IPFS, we have a built one that leverages the Browser transports).
|
||||
|
||||
**Example:**
|
||||
|
||||
```JavaScript
|
||||
// Creating a bundle that adds:
|
||||
// transport: websockets + tcp
|
||||
// stream-muxing: spdy & mplex
|
||||
// crypto-channel: secio
|
||||
// discovery: multicast-dns
|
||||
|
||||
const Libp2p = require('libp2p')
|
||||
const TCP = require('libp2p-tcp')
|
||||
const WS = require('libp2p-websockets')
|
||||
const SPDY = require('libp2p-spdy')
|
||||
const MPLEX = require('libp2p-mplex')
|
||||
const SECIO = require('libp2p-secio')
|
||||
const MulticastDNS = require('libp2p-mdns')
|
||||
const DHT = require('libp2p-kad-dht')
|
||||
const defaultsDeep = require('@nodeutils/defaults-deep')
|
||||
const Protector = require('libp2p-pnet')
|
||||
const DelegatedPeerRouter = require('libp2p-delegated-peer-routing')
|
||||
const DelegatedContentRouter = require('libp2p-delegated-content-routing')
|
||||
|
||||
class Node extends Libp2p {
|
||||
constructor (_options) {
|
||||
const peerInfo = _options.peerInfo
|
||||
const defaults = {
|
||||
// The libp2p modules for this libp2p bundle
|
||||
modules: {
|
||||
transport: [
|
||||
TCP,
|
||||
new WS() // It can take instances too!
|
||||
],
|
||||
streamMuxer: [
|
||||
SPDY,
|
||||
MPLEX
|
||||
],
|
||||
connEncryption: [
|
||||
SECIO
|
||||
],
|
||||
/** Encryption for private networks. Needs additional private key to work **/
|
||||
// connProtector: new Protector(/*protector specific opts*/),
|
||||
/** Enable custom content routers, such as delegated routing **/
|
||||
// contentRouting: [
|
||||
// new DelegatedContentRouter(peerInfo.id)
|
||||
// ],
|
||||
/** Enable custom peer routers, such as delegated routing **/
|
||||
// peerRouting: [
|
||||
// new DelegatedPeerRouter()
|
||||
// ],
|
||||
peerDiscovery: [
|
||||
MulticastDNS
|
||||
],
|
||||
dht: DHT // DHT enables PeerRouting, ContentRouting and DHT itself components
|
||||
},
|
||||
|
||||
// libp2p config options (typically found on a config.json)
|
||||
config: { // The config object is the part of the config that can go into a file, config.json.
|
||||
peerDiscovery: {
|
||||
autoDial: true, // Auto connect to discovered peers (limited by ConnectionManager minPeers)
|
||||
mdns: { // mdns options
|
||||
interval: 1000, // ms
|
||||
enabled: true
|
||||
},
|
||||
webrtcStar: { // webrtc-star options
|
||||
interval: 1000, // ms
|
||||
enabled: false
|
||||
}
|
||||
// .. other discovery module options.
|
||||
},
|
||||
relay: { // Circuit Relay options
|
||||
enabled: true,
|
||||
hop: {
|
||||
enabled: false,
|
||||
active: false
|
||||
}
|
||||
},
|
||||
dht: {
|
||||
kBucketSize: 20,
|
||||
enabled: true,
|
||||
randomWalk: {
|
||||
enabled: true, // Allows to disable discovery (enabled by default)
|
||||
interval: 300e3,
|
||||
timeout: 10e3
|
||||
}
|
||||
},
|
||||
// Enable/Disable Experimental features
|
||||
EXPERIMENTAL: { // Experimental features ("behind a flag")
|
||||
pubsub: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// overload any defaults of your bundle using https://github.com/nodeutils/defaults-deep
|
||||
super(defaultsDeep(_options, defaults))
|
||||
}
|
||||
}
|
||||
|
||||
// Now all the nodes you create, will have TCP, WebSockets, SPDY, MPLEX, SECIO and MulticastDNS support.
|
||||
```
|
||||
For all the information on how you can configure libp2p see [CONFIGURATION.md](./doc/CONFIGURATION.md).
|
||||
|
||||
### API
|
||||
|
||||
#### Create a Node - `Libp2p.createLibp2p(options, callback)`
|
||||
The specification is available on [API.md](./doc/API.md).
|
||||
|
||||
> Behaves exactly like `new Libp2p(options)`, but doesn't require a PeerInfo. One will be generated instead
|
||||
### Tutorials and Examples
|
||||
|
||||
```js
|
||||
const { createLibp2p } = require('libp2p')
|
||||
createLibp2p(options, (err, libp2p) => {
|
||||
if (err) throw err
|
||||
libp2p.start((err) => {
|
||||
if (err) throw err
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
- `options`: Object of libp2p configuration options
|
||||
- `callback`: Function with signature `function (Error, Libp2p) {}`
|
||||
|
||||
#### Create a Node alternative - `new Libp2p(options)`
|
||||
|
||||
> Creates an instance of Libp2p with a custom `PeerInfo` provided via `options.peerInfo`.
|
||||
|
||||
Required keys in the `options` object:
|
||||
|
||||
- `peerInfo`: instance of [PeerInfo][] that contains the [PeerId][], Keys and [multiaddrs][multiaddr] of the libp2p Node.
|
||||
- `modules.transport`: An array that must include at least 1 transport, such as `libp2p-tcp`.
|
||||
|
||||
#### `libp2p.start(callback)`
|
||||
|
||||
> Start the libp2p Node.
|
||||
|
||||
`callback` following signature `function (err) {}`, where `err` is an Error in case starting the node fails.
|
||||
|
||||
#### `libp2p.stop(callback)`
|
||||
|
||||
> Stop the libp2p Node.
|
||||
|
||||
`callback` following signature `function (err) {}`, where `err` is an Error in case stopping the node fails.
|
||||
|
||||
#### `libp2p.dial(peer, callback)`
|
||||
|
||||
> Dials to another peer in the network, establishes the connection.
|
||||
|
||||
- `peer`: can be an instance of [PeerInfo][], [PeerId][], [multiaddr][], or a multiaddr string
|
||||
- `callback` following signature `function (err, conn) {}`, where `err` is an Error in of failure to dial the connection and `conn` is a [Connection][] instance in case of a protocol selected, if not it is undefined.
|
||||
|
||||
#### `libp2p.dialProtocol(peer, protocol, callback)`
|
||||
|
||||
> Dials to another peer in the network and selects a protocol to talk with that peer.
|
||||
|
||||
- `peer`: can be an instance of [PeerInfo][], [PeerId][], [multiaddr][], or a multiaddr string
|
||||
- `protocol`: String that defines the protocol (e.g '/ipfs/bitswap/1.1.0')
|
||||
- `callback`: Function with signature `function (err, conn) {}`, where `conn` is a [Connection](https://github.com/libp2p/interface-connection) object
|
||||
|
||||
`callback` following signature `function (err, conn) {}`, where `err` is an Error in of failure to dial the connection and `conn` is a [Connection][] instance in case of a protocol selected, if not it is undefined.
|
||||
|
||||
#### `libp2p.dialFSM(peer, protocol, callback)`
|
||||
|
||||
> Behaves like `.dial` and `.dialProtocol` but calls back with a Connection State Machine
|
||||
|
||||
- `peer`: can be an instance of [PeerInfo][], [PeerId][], [multiaddr][], or a multiaddr string
|
||||
- `protocol`: an optional String that defines the protocol (e.g '/ipfs/bitswap/1.1.0')
|
||||
- `callback`: following signature `function (err, connFSM) {}`, where `connFSM` is a [Connection State Machine](https://github.com/libp2p/js-libp2p-switch#connection-state-machine)
|
||||
|
||||
#### `libp2p.hangUp(peer, callback)`
|
||||
|
||||
> Closes an open connection with a peer, graciously.
|
||||
|
||||
- `peer`: can be an instance of [PeerInfo][], [PeerId][] or [multiaddr][]
|
||||
|
||||
`callback` following signature `function (err) {}`, where `err` is an Error in case stopping the node fails.
|
||||
|
||||
#### `libp2p.peerRouting.findPeer(id, options, callback)`
|
||||
|
||||
> Looks up for multiaddrs of a peer in the DHT
|
||||
|
||||
- `id`: instance of [PeerId][]
|
||||
- `options`: object of options
|
||||
- `options.maxTimeout`: Number milliseconds
|
||||
|
||||
#### `libp2p.contentRouting.findProviders(key, options, callback)`
|
||||
|
||||
- `key`: Buffer
|
||||
- `options`: object of options
|
||||
- `options.maxTimeout`: Number milliseconds
|
||||
- `options.maxNumProviders` maximum number of providers to find
|
||||
|
||||
#### `libp2p.contentRouting.provide(key, callback)`
|
||||
|
||||
- `key`: Buffer
|
||||
|
||||
#### `libp2p.handle(protocol, handlerFunc [, matchFunc])`
|
||||
|
||||
> Handle new protocol
|
||||
|
||||
- `protocol`: String that defines the protocol (e.g '/ipfs/bitswap/1.1.0')
|
||||
- `handlerFunc`: following signature `function (protocol, conn) {}`, where `conn` is a [Connection](https://github.com/libp2p/interface-connection) object
|
||||
- `matchFunc`: Function for matching on protocol (exact matching, semver, etc). Default to exact match.
|
||||
|
||||
#### `libp2p.unhandle(protocol)`
|
||||
|
||||
> Stop handling protocol
|
||||
|
||||
- `protocol`: String that defines the protocol (e.g '/ipfs/bitswap/1.1.0')
|
||||
|
||||
#### Events
|
||||
|
||||
##### `libp2p.on('start', () => {})`
|
||||
|
||||
> Libp2p has started, along with all its services.
|
||||
|
||||
##### `libp2p.on('stop', () => {})`
|
||||
|
||||
> Libp2p has stopped, along with all its services.
|
||||
|
||||
##### `libp2p.on('error', (err) => {})`
|
||||
|
||||
> An error has occurred
|
||||
|
||||
- `err`: instance of `Error`
|
||||
|
||||
##### `libp2p.on('peer:discovery', (peer) => {})`
|
||||
|
||||
> Peer has been discovered.
|
||||
|
||||
If `autoDial` is `true`, applications should **not** attempt to connect to the peer
|
||||
unless they are performing a specific action. See [peer discovery and auto dial](./PEER_DISCOVERY.md) for more information.
|
||||
|
||||
- `peer`: instance of [PeerInfo][]
|
||||
|
||||
##### `libp2p.on('peer:connect', (peer) => {})`
|
||||
|
||||
> We have a new muxed connection to a peer
|
||||
|
||||
- `peer`: instance of [PeerInfo][]
|
||||
|
||||
##### `libp2p.on('peer:disconnect', (peer) => {})`
|
||||
|
||||
> We have closed a connection to a peer
|
||||
|
||||
- `peer`: instance of [PeerInfo][]
|
||||
|
||||
##### `libp2p.on('connection:start', (peer) => {})`
|
||||
|
||||
> We created a new connection to a peer
|
||||
|
||||
- `peer`: instance of [PeerInfo][]
|
||||
|
||||
##### `libp2p.on('connection:end', (peer) => {})`
|
||||
|
||||
> We closed a connection to a peer
|
||||
|
||||
- `peer`: instance of [PeerInfo][]
|
||||
|
||||
#### `libp2p.isStarted()`
|
||||
|
||||
> Check if libp2p is started
|
||||
|
||||
#### `libp2p.ping(peer [, options], callback)`
|
||||
|
||||
> Ping a node in the network
|
||||
|
||||
#### `libp2p.peerBook`
|
||||
|
||||
> PeerBook instance of the node
|
||||
|
||||
#### `libp2p.peerInfo`
|
||||
|
||||
> PeerInfo instance of the node
|
||||
|
||||
#### `libp2p.pubsub`
|
||||
|
||||
> Same API as IPFS PubSub, defined in the [CORE API Spec](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/PUBSUB.md). Just replace `ipfs` by `libp2p` and you are golden.
|
||||
|
||||
---------------------
|
||||
|
||||
`DHT methods also exposed for the time being`
|
||||
|
||||
#### `libp2p.dht.put(key, value, callback)`
|
||||
|
||||
- `key`: Buffer
|
||||
- `value`: Buffer
|
||||
|
||||
#### `libp2p.dht.get(key, options, callback)`
|
||||
|
||||
- `key`: Buffer
|
||||
- `options`: object of options
|
||||
- `options.maxTimeout`: Number milliseconds
|
||||
|
||||
#### `libp2p.dht.getMany(key, nVals, options, callback)`
|
||||
|
||||
- `key`: Buffer
|
||||
- `nVals`: Number
|
||||
- `options`: object of options
|
||||
- `options.maxTimeout`: Number milliseconds
|
||||
|
||||
[PeerInfo]: https://github.com/libp2p/js-peer-info
|
||||
[PeerId]: https://github.com/libp2p/js-peer-id
|
||||
[PeerBook]: https://github.com/libp2p/js-peer-book
|
||||
[multiaddr]: https://github.com/multiformats/js-multiaddr
|
||||
[Connection]: https://github.com/libp2p/interface-connection
|
||||
|
||||
-------
|
||||
|
||||
### Switch Stats API
|
||||
|
||||
##### `libp2p.stats.emit('update')`
|
||||
|
||||
Every time any stat value changes, this object emits an `update` event.
|
||||
|
||||
#### Global stats
|
||||
|
||||
##### `libp2p.stats.global.snapshot`
|
||||
|
||||
Should return a stats snapshot, which is an object containing the following keys and respective values:
|
||||
|
||||
- dataSent: amount of bytes sent, [Big](https://github.com/MikeMcl/big.js#readme) number
|
||||
- dataReceived: amount of bytes received, [Big](https://github.com/MikeMcl/big.js#readme) number
|
||||
|
||||
##### `libp2p.stats.global.movingAverages`
|
||||
|
||||
Returns an object containing the following keys:
|
||||
|
||||
- dataSent
|
||||
- dataReceived
|
||||
|
||||
Each one of them contains an object that has a key for each interval (`60000`, `300000` and `900000` miliseconds).
|
||||
|
||||
Each one of these values is [an exponential moving-average instance](https://github.com/pgte/moving-average#readme).
|
||||
|
||||
#### Per-transport stats
|
||||
|
||||
##### `libp2p.stats.transports()`
|
||||
|
||||
Returns an array containing the tags (string) for each observed transport.
|
||||
|
||||
##### `libp2p.stats.forTransport(transportTag).snapshot`
|
||||
|
||||
Should return a stats snapshot, which is an object containing the following keys and respective values:
|
||||
|
||||
- dataSent: amount of bytes sent, [Big](https://github.com/MikeMcl/big.js#readme) number
|
||||
- dataReceived: amount of bytes received, [Big](https://github.com/MikeMcl/big.js#readme) number
|
||||
|
||||
##### `libp2p.stats.forTransport(transportTag).movingAverages`
|
||||
|
||||
Returns an object containing the following keys:
|
||||
|
||||
dataSent
|
||||
dataReceived
|
||||
|
||||
Each one of them contains an object that has a key for each interval (`60000`, `300000` and `900000` miliseconds).
|
||||
|
||||
Each one of these values is [an exponential moving-average instance](https://github.com/pgte/moving-average#readme).
|
||||
|
||||
#### Per-protocol stats
|
||||
|
||||
##### `libp2p.stats.protocols()`
|
||||
|
||||
Returns an array containing the tags (string) for each observed protocol.
|
||||
|
||||
##### `libp2p.stats.forProtocol(protocolTag).snapshot`
|
||||
|
||||
Should return a stats snapshot, which is an object containing the following keys and respective values:
|
||||
|
||||
- dataSent: amount of bytes sent, [Big](https://github.com/MikeMcl/big.js#readme) number
|
||||
- dataReceived: amount of bytes received, [Big](https://github.com/MikeMcl/big.js#readme) number
|
||||
|
||||
|
||||
##### `libp2p.stats.forProtocol(protocolTag).movingAverages`
|
||||
|
||||
Returns an object containing the following keys:
|
||||
|
||||
- dataSent
|
||||
- dataReceived
|
||||
|
||||
Each one of them contains an object that has a key for each interval (`60000`, `300000` and `900000` miliseconds).
|
||||
|
||||
Each one of these values is [an exponential moving-average instance](https://github.com/pgte/moving-average#readme).
|
||||
|
||||
#### Per-peer stats
|
||||
|
||||
##### `libp2p.stats.peers()`
|
||||
|
||||
Returns an array containing the peerIDs (B58-encoded string) for each observed peer.
|
||||
|
||||
##### `libp2p.stats.forPeer(peerId:String).snapshot`
|
||||
|
||||
Should return a stats snapshot, which is an object containing the following keys and respective values:
|
||||
|
||||
- dataSent: amount of bytes sent, [Big](https://github.com/MikeMcl/big.js#readme) number
|
||||
- dataReceived: amount of bytes received, [Big](https://github.com/MikeMcl/big.js#readme) number
|
||||
|
||||
|
||||
##### `libp2p.stats.forPeer(peerId:String).movingAverages`
|
||||
|
||||
Returns an object containing the following keys:
|
||||
|
||||
- dataSent
|
||||
- dataReceived
|
||||
|
||||
Each one of them contains an object that has a key for each interval (`60000`, `300000` and `900000` miliseconds).
|
||||
|
||||
Each one of these values is [an exponential moving-average instance](https://github.com/pgte/moving-average#readme).
|
||||
|
||||
#### Stats update interval
|
||||
|
||||
Stats are not updated in real-time. Instead, measurements are buffered and stats are updated at an interval. The maximum interval can be defined through the `Switch` constructor option `stats.computeThrottleTimeout`, defined in miliseconds.
|
||||
|
||||
### Private Networks
|
||||
|
||||
#### Enforcement
|
||||
|
||||
Libp2p provides support for connection protection, such as for private networks. You can enforce network protection by setting the environment variable `LIBP2P_FORCE_PNET=1`. When this variable is on, if no protector is set via `options.connProtector`, Libp2p will throw an error upon creation.
|
||||
|
||||
#### Protectors
|
||||
|
||||
Some available network protectors:
|
||||
* [libp2p-pnet](https://github.com/libp2p/js-libp2p-pnet)
|
||||
You can find multiple examples on the [examples folder](./examples) that will guide you through using libp2p for several scenarios.
|
||||
|
||||
## Development
|
||||
|
||||
**Clone and install dependencies:**
|
||||
|
||||
```sh
|
||||
> git clone https://github.com/ipfs/js-ipfs.git
|
||||
> cd js-ipfs
|
||||
> git clone https://github.com/libp2p/js-libp2p.git
|
||||
> cd js-libp2p
|
||||
> npm install
|
||||
```
|
||||
|
||||
@ -555,67 +126,50 @@ List of packages currently in existence for libp2p
|
||||
|
||||
| Package | Version | Deps | CI | Coverage | Lead Maintainer |
|
||||
| ---------|---------|---------|---------|---------|--------- |
|
||||
| **Libp2p** |
|
||||
| [`interface-libp2p`](//github.com/libp2p/interface-libp2p) | [](//github.com/libp2p/interface-libp2p/releases) | [](https://david-dm.org/libp2p/interface-libp2p) | [](https://travis-ci.com/libp2p/interface-libp2p) | [](https://codecov.io/gh/libp2p/interface-libp2p) | N/A |
|
||||
| [`libp2p`](//github.com/libp2p/js-libp2p) | [](//github.com/libp2p/js-libp2p/releases) | [](https://david-dm.org/libp2p/js-libp2p) | [](https://travis-ci.com/libp2p/js-libp2p) | [](https://codecov.io/gh/libp2p/js-libp2p) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| **Connection** |
|
||||
| [`interface-connection`](//github.com/libp2p/interface-connection) | [](//github.com/libp2p/interface-connection/releases) | [](https://david-dm.org/libp2p/interface-connection) | [](https://travis-ci.com/libp2p/interface-connection) | [](https://codecov.io/gh/libp2p/interface-connection) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| **Transport** |
|
||||
| [`interface-transport`](//github.com/libp2p/interface-transport) | [](//github.com/libp2p/interface-transport/releases) | [](https://david-dm.org/libp2p/interface-transport) | [](https://travis-ci.com/libp2p/interface-transport) | [](https://codecov.io/gh/libp2p/interface-transport) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| [`libp2p-tcp`](//github.com/libp2p/js-libp2p-tcp) | [](//github.com/libp2p/js-libp2p-tcp/releases) | [](https://david-dm.org/libp2p/js-libp2p-tcp) | [](https://travis-ci.com/libp2p/js-libp2p-tcp) | [](https://codecov.io/gh/libp2p/js-libp2p-tcp) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| [`libp2p-udp`](//github.com/libp2p/js-libp2p-udp) | [](//github.com/libp2p/js-libp2p-udp/releases) | [](https://david-dm.org/libp2p/js-libp2p-udp) | [](https://travis-ci.com/libp2p/js-libp2p-udp) | [](https://codecov.io/gh/libp2p/js-libp2p-udp) | N/A |
|
||||
| [`libp2p-udt`](//github.com/libp2p/js-libp2p-udt) | [](//github.com/libp2p/js-libp2p-udt/releases) | [](https://david-dm.org/libp2p/js-libp2p-udt) | [](https://travis-ci.com/libp2p/js-libp2p-udt) | [](https://codecov.io/gh/libp2p/js-libp2p-udt) | N/A |
|
||||
| [`libp2p-utp`](//github.com/libp2p/js-libp2p-utp) | [](//github.com/libp2p/js-libp2p-utp/releases) | [](https://david-dm.org/libp2p/js-libp2p-utp) | [](https://travis-ci.com/libp2p/js-libp2p-utp) | [](https://codecov.io/gh/libp2p/js-libp2p-utp) | N/A |
|
||||
| [`libp2p-webrtc-direct`](//github.com/libp2p/js-libp2p-webrtc-direct) | [](//github.com/libp2p/js-libp2p-webrtc-direct/releases) | [](https://david-dm.org/libp2p/js-libp2p-webrtc-direct) | [](https://travis-ci.com/libp2p/js-libp2p-webrtc-direct) | [](https://codecov.io/gh/libp2p/js-libp2p-webrtc-direct) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
|
||||
| [`libp2p-webrtc-star`](//github.com/libp2p/js-libp2p-webrtc-star) | [](//github.com/libp2p/js-libp2p-webrtc-star/releases) | [](https://david-dm.org/libp2p/js-libp2p-webrtc-star) | [](https://travis-ci.com/libp2p/js-libp2p-webrtc-star) | [](https://codecov.io/gh/libp2p/js-libp2p-webrtc-star) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
|
||||
| [`libp2p-websockets`](//github.com/libp2p/js-libp2p-websockets) | [](//github.com/libp2p/js-libp2p-websockets/releases) | [](https://david-dm.org/libp2p/js-libp2p-websockets) | [](https://travis-ci.com/libp2p/js-libp2p-websockets) | [](https://codecov.io/gh/libp2p/js-libp2p-websockets) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| [`libp2p-websocket-star`](//github.com/libp2p/js-libp2p-websocket-star) | [](//github.com/libp2p/js-libp2p-websocket-star/releases) | [](https://david-dm.org/libp2p/js-libp2p-websocket-star) | [](https://travis-ci.com/libp2p/js-libp2p-websocket-star) | [](https://codecov.io/gh/libp2p/js-libp2p-websocket-star) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| [`libp2p-websocket-star-rendezvous`](//github.com/libp2p/js-libp2p-websocket-star-rendezvous) | [](//github.com/libp2p/js-libp2p-websocket-star-rendezvous/releases) | [](https://david-dm.org/libp2p/js-libp2p-websocket-star-rendezvous) | [](https://travis-ci.com/libp2p/js-libp2p-websocket-star-rendezvous) | [](https://codecov.io/gh/libp2p/js-libp2p-websocket-star-rendezvous) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| **Crypto Channels** |
|
||||
| [`libp2p-secio`](//github.com/libp2p/js-libp2p-secio) | [](//github.com/libp2p/js-libp2p-secio/releases) | [](https://david-dm.org/libp2p/js-libp2p-secio) | [](https://travis-ci.com/libp2p/js-libp2p-secio) | [](https://codecov.io/gh/libp2p/js-libp2p-secio) | [Friedel Ziegelmayer](mailto:dignifiedquire@gmail.com) |
|
||||
| **Stream Muxers** |
|
||||
| [`interface-stream-muxer`](//github.com/libp2p/interface-stream-muxer) | [](//github.com/libp2p/interface-stream-muxer/releases) | [](https://david-dm.org/libp2p/interface-stream-muxer) | [](https://travis-ci.com/libp2p/interface-stream-muxer) | [](https://codecov.io/gh/libp2p/interface-stream-muxer) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| [`libp2p-mplex`](//github.com/libp2p/js-libp2p-mplex) | [](//github.com/libp2p/js-libp2p-mplex/releases) | [](https://david-dm.org/libp2p/js-libp2p-mplex) | [](https://travis-ci.com/libp2p/js-libp2p-mplex) | [](https://codecov.io/gh/libp2p/js-libp2p-mplex) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
|
||||
| [`libp2p-spdy`](//github.com/libp2p/js-libp2p-spdy) | [](//github.com/libp2p/js-libp2p-spdy/releases) | [](https://david-dm.org/libp2p/js-libp2p-spdy) | [](https://travis-ci.com/libp2p/js-libp2p-spdy) | [](https://codecov.io/gh/libp2p/js-libp2p-spdy) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| **Discovery** |
|
||||
| [`interface-peer-discovery`](//github.com/libp2p/interface-peer-discovery) | [](//github.com/libp2p/interface-peer-discovery/releases) | [](https://david-dm.org/libp2p/interface-peer-discovery) | [](https://travis-ci.com/libp2p/interface-peer-discovery) | [](https://codecov.io/gh/libp2p/interface-peer-discovery) | N/A |
|
||||
| [`libp2p-bootstrap`](//github.com/libp2p/js-libp2p-bootstrap) | [](//github.com/libp2p/js-libp2p-bootstrap/releases) | [](https://david-dm.org/libp2p/js-libp2p-bootstrap) | [](https://travis-ci.com/libp2p/js-libp2p-bootstrap) | [](https://codecov.io/gh/libp2p/js-libp2p-bootstrap) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
|
||||
| [`libp2p-kad-dht`](//github.com/libp2p/js-libp2p-kad-dht) | [](//github.com/libp2p/js-libp2p-kad-dht/releases) | [](https://david-dm.org/libp2p/js-libp2p-kad-dht) | [](https://travis-ci.com/libp2p/js-libp2p-kad-dht) | [](https://codecov.io/gh/libp2p/js-libp2p-kad-dht) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
|
||||
| [`libp2p-mdns`](//github.com/libp2p/js-libp2p-mdns) | [](//github.com/libp2p/js-libp2p-mdns/releases) | [](https://david-dm.org/libp2p/js-libp2p-mdns) | [](https://travis-ci.com/libp2p/js-libp2p-mdns) | [](https://codecov.io/gh/libp2p/js-libp2p-mdns) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| [`libp2p-rendezvous`](//github.com/libp2p/js-libp2p-rendezvous) | [](//github.com/libp2p/js-libp2p-rendezvous/releases) | [](https://david-dm.org/libp2p/js-libp2p-rendezvous) | [](https://travis-ci.com/libp2p/js-libp2p-rendezvous) | [](https://codecov.io/gh/libp2p/js-libp2p-rendezvous) | N/A |
|
||||
| [`libp2p-webrtc-star`](//github.com/libp2p/js-libp2p-webrtc-star) | [](//github.com/libp2p/js-libp2p-webrtc-star/releases) | [](https://david-dm.org/libp2p/js-libp2p-webrtc-star) | [](https://travis-ci.com/libp2p/js-libp2p-webrtc-star) | [](https://codecov.io/gh/libp2p/js-libp2p-webrtc-star) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
|
||||
| [`libp2p-websocket-star`](//github.com/libp2p/js-libp2p-websocket-star) | [](//github.com/libp2p/js-libp2p-websocket-star/releases) | [](https://david-dm.org/libp2p/js-libp2p-websocket-star) | [](https://travis-ci.com/libp2p/js-libp2p-websocket-star) | [](https://codecov.io/gh/libp2p/js-libp2p-websocket-star) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| **NAT Traversal** |
|
||||
| [`libp2p-circuit`](//github.com/libp2p/js-libp2p-circuit) | [](//github.com/libp2p/js-libp2p-circuit/releases) | [](https://david-dm.org/libp2p/js-libp2p-circuit) | [](https://travis-ci.com/libp2p/js-libp2p-circuit) | [](https://codecov.io/gh/libp2p/js-libp2p-circuit) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| [`libp2p-nat-mngr`](//github.com/libp2p/js-libp2p-nat-mngr) | [](//github.com/libp2p/js-libp2p-nat-mngr/releases) | [](https://david-dm.org/libp2p/js-libp2p-nat-mngr) | [](https://travis-ci.com/libp2p/js-libp2p-nat-mngr) | [](https://codecov.io/gh/libp2p/js-libp2p-nat-mngr) | N/A |
|
||||
| **Data Types** |
|
||||
| [`peer-book`](//github.com/libp2p/js-peer-book) | [](//github.com/libp2p/js-peer-book/releases) | [](https://david-dm.org/libp2p/js-peer-book) | [](https://travis-ci.com/libp2p/js-peer-book) | [](https://codecov.io/gh/libp2p/js-peer-book) | [Pedro Teixeira](mailto:i@pgte.me) |
|
||||
| [`peer-id`](//github.com/libp2p/js-peer-id) | [](//github.com/libp2p/js-peer-id/releases) | [](https://david-dm.org/libp2p/js-peer-id) | [](https://travis-ci.com/libp2p/js-peer-id) | [](https://codecov.io/gh/libp2p/js-peer-id) | [Pedro Teixeira](mailto:i@pgte.me) |
|
||||
| [`peer-info`](//github.com/libp2p/js-peer-info) | [](//github.com/libp2p/js-peer-info/releases) | [](https://david-dm.org/libp2p/js-peer-info) | [](https://travis-ci.com/libp2p/js-peer-info) | [](https://codecov.io/gh/libp2p/js-peer-info) | [Pedro Teixeira](mailto:i@pgte.me) |
|
||||
| **Content Routing** |
|
||||
| [`interface-content-routing`](//github.com/libp2p/interface-content-routing) | [](//github.com/libp2p/interface-content-routing/releases) | [](https://david-dm.org/libp2p/interface-content-routing) | [](https://travis-ci.com/libp2p/interface-content-routing) | [](https://codecov.io/gh/libp2p/interface-content-routing) | N/A |
|
||||
| [`libp2p-delegated-content-routing`](//github.com/libp2p/js-libp2p-delegated-content-routing) | [](//github.com/libp2p/js-libp2p-delegated-content-routing/releases) | [](https://david-dm.org/libp2p/js-libp2p-delegated-content-routing) | [](https://travis-ci.com/libp2p/js-libp2p-delegated-content-routing) | [](https://codecov.io/gh/libp2p/js-libp2p-delegated-content-routing) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| [`libp2p-kad-dht`](//github.com/libp2p/js-libp2p-kad-dht) | [](//github.com/libp2p/js-libp2p-kad-dht/releases) | [](https://david-dm.org/libp2p/js-libp2p-kad-dht) | [](https://travis-ci.com/libp2p/js-libp2p-kad-dht) | [](https://codecov.io/gh/libp2p/js-libp2p-kad-dht) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
|
||||
| **Peer Routing** |
|
||||
| [`interface-peer-routing`](//github.com/libp2p/interface-peer-routing) | [](//github.com/libp2p/interface-peer-routing/releases) | [](https://david-dm.org/libp2p/interface-peer-routing) | [](https://travis-ci.com/libp2p/interface-peer-routing) | [](https://codecov.io/gh/libp2p/interface-peer-routing) | N/A |
|
||||
| [`libp2p-delegated-peer-routing`](//github.com/libp2p/js-libp2p-delegated-peer-routing) | [](//github.com/libp2p/js-libp2p-delegated-peer-routing/releases) | [](https://david-dm.org/libp2p/js-libp2p-delegated-peer-routing) | [](https://travis-ci.com/libp2p/js-libp2p-delegated-peer-routing) | [](https://codecov.io/gh/libp2p/js-libp2p-delegated-peer-routing) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| [`libp2p-kad-dht`](//github.com/libp2p/js-libp2p-kad-dht) | [](//github.com/libp2p/js-libp2p-kad-dht/releases) | [](https://david-dm.org/libp2p/js-libp2p-kad-dht) | [](https://travis-ci.com/libp2p/js-libp2p-kad-dht) | [](https://codecov.io/gh/libp2p/js-libp2p-kad-dht) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
|
||||
| **Record Store** |
|
||||
| [`interface-record-store`](//github.com/libp2p/interface-record-store) | [](//github.com/libp2p/interface-record-store/releases) | [](https://david-dm.org/libp2p/interface-record-store) | [](https://travis-ci.com/libp2p/interface-record-store) | [](https://codecov.io/gh/libp2p/interface-record-store) | N/A |
|
||||
| [`libp2p-record`](//github.com/libp2p/js-libp2p-record) | [](//github.com/libp2p/js-libp2p-record/releases) | [](https://david-dm.org/libp2p/js-libp2p-record) | [](https://travis-ci.com/libp2p/js-libp2p-record) | [](https://codecov.io/gh/libp2p/js-libp2p-record) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
|
||||
| **Generics** |
|
||||
| [`libp2p-connection-manager`](//github.com/libp2p/js-libp2p-connection-manager) | [](//github.com/libp2p/js-libp2p-connection-manager/releases) | [](https://david-dm.org/libp2p/js-libp2p-connection-manager) | [](https://travis-ci.com/libp2p/js-libp2p-connection-manager) | [](https://codecov.io/gh/libp2p/js-libp2p-connection-manager) | N/A |
|
||||
| [`libp2p-crypto`](//github.com/libp2p/js-libp2p-crypto) | [](//github.com/libp2p/js-libp2p-crypto/releases) | [](https://david-dm.org/libp2p/js-libp2p-crypto) | [](https://travis-ci.com/libp2p/js-libp2p-crypto) | [](https://codecov.io/gh/libp2p/js-libp2p-crypto) | [Friedel Ziegelmayer](mailto:dignifiedquire@gmail.com) |
|
||||
| [`libp2p-crypto-secp256k1`](//github.com/libp2p/js-libp2p-crypto-secp256k1) | [](//github.com/libp2p/js-libp2p-crypto-secp256k1/releases) | [](https://david-dm.org/libp2p/js-libp2p-crypto-secp256k1) | [](https://travis-ci.com/libp2p/js-libp2p-crypto-secp256k1) | [](https://codecov.io/gh/libp2p/js-libp2p-crypto-secp256k1) | [Friedel Ziegelmayer](mailto:dignifiedquire@gmail.com) |
|
||||
| [`libp2p-switch`](//github.com/libp2p/js-libp2p-switch) | [](//github.com/libp2p/js-libp2p-switch/releases) | [](https://david-dm.org/libp2p/js-libp2p-switch) | [](https://travis-ci.com/libp2p/js-libp2p-switch) | [](https://codecov.io/gh/libp2p/js-libp2p-switch) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| **Extensions** |
|
||||
| [`libp2p-floodsub`](//github.com/libp2p/js-libp2p-floodsub) | [](//github.com/libp2p/js-libp2p-floodsub/releases) | [](https://david-dm.org/libp2p/js-libp2p-floodsub) | [](https://travis-ci.com/libp2p/js-libp2p-floodsub) | [](https://codecov.io/gh/libp2p/js-libp2p-floodsub) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
|
||||
| [`libp2p-identify`](//github.com/libp2p/js-libp2p-identify) | [](//github.com/libp2p/js-libp2p-identify/releases) | [](https://david-dm.org/libp2p/js-libp2p-identify) | [](https://travis-ci.com/libp2p/js-libp2p-identify) | [](https://codecov.io/gh/libp2p/js-libp2p-identify) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| [`libp2p-keychain`](//github.com/libp2p/js-libp2p-keychain) | [](//github.com/libp2p/js-libp2p-keychain/releases) | [](https://david-dm.org/libp2p/js-libp2p-keychain) | [](https://travis-ci.com/libp2p/js-libp2p-keychain) | [](https://codecov.io/gh/libp2p/js-libp2p-keychain) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
|
||||
| [`libp2p-ping`](//github.com/libp2p/js-libp2p-ping) | [](//github.com/libp2p/js-libp2p-ping/releases) | [](https://david-dm.org/libp2p/js-libp2p-ping) | [](https://travis-ci.com/libp2p/js-libp2p-ping) | [](https://codecov.io/gh/libp2p/js-libp2p-ping) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| [`libp2p-pnet`](//github.com/libp2p/js-libp2p-pnet) | [](//github.com/libp2p/js-libp2p-pnet/releases) | [](https://david-dm.org/libp2p/js-libp2p-pnet) | [](https://travis-ci.com/libp2p/js-libp2p-pnet) | [](https://codecov.io/gh/libp2p/js-libp2p-pnet) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| **Utilities** |
|
||||
| [`p2pcat`](//github.com/libp2p/js-p2pcat) | [](//github.com/libp2p/js-p2pcat/releases) | [](https://david-dm.org/libp2p/js-p2pcat) | [](https://travis-ci.com/libp2p/js-p2pcat) | [](https://codecov.io/gh/libp2p/js-p2pcat) | N/A |
|
||||
| **libp2p** |
|
||||
| [`libp2p`](//github.com/libp2p/js-libp2p) | [](//github.com/libp2p/js-libp2p/releases) | [](https://david-dm.org/libp2p/js-libp2p) | [](https://travis-ci.com/libp2p/js-libp2p) | [](https://codecov.io/gh/libp2p/js-libp2p) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| [`libp2p-daemon`](//github.com/libp2p/js-libp2p-daemon) | [](//github.com/libp2p/js-libp2p-daemon/releases) | [](https://david-dm.org/libp2p/js-libp2p-daemon) | [](https://travis-ci.com/libp2p/js-libp2p-daemon) | [](https://codecov.io/gh/libp2p/js-libp2p-daemon) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| [`libp2p-daemon-client`](//github.com/libp2p/js-libp2p-daemon-client) | [](//github.com/libp2p/js-libp2p-daemon-client/releases) | [](https://david-dm.org/libp2p/js-libp2p-daemon-client) | [](https://travis-ci.com/libp2p/js-libp2p-daemon-client) | [](https://codecov.io/gh/libp2p/js-libp2p-daemon-client) | [Vasco Santos](mailto:santos.vasco10@gmail.com) |
|
||||
| [`libp2p-interfaces`](//github.com/libp2p/js-interfaces) | [](//github.com/libp2p/js-interfaces/releases) | [](https://david-dm.org/libp2p/js-interfaces) | [](https://travis-ci.com/libp2p/js-interfaces) | [](https://codecov.io/gh/libp2p/js-interfaces) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| [`interop-libp2p`](//github.com/libp2p/interop) | [](//github.com/libp2p/interop/releases) | [](https://david-dm.org/libp2p/interop) | [](https://travis-ci.com/libp2p/interop) | [](https://codecov.io/gh/libp2p/interop) | [Vasco Santos](mailto:santos.vasco10@gmail.com) |
|
||||
| **transports** |
|
||||
| [`libp2p-tcp`](//github.com/libp2p/js-libp2p-tcp) | [](//github.com/libp2p/js-libp2p-tcp/releases) | [](https://david-dm.org/libp2p/js-libp2p-tcp) | [](https://travis-ci.com/libp2p/js-libp2p-tcp) | [](https://codecov.io/gh/libp2p/js-libp2p-tcp) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| [`libp2p-utp`](//github.com/libp2p/js-libp2p-utp) | [](//github.com/libp2p/js-libp2p-utp/releases) | [](https://david-dm.org/libp2p/js-libp2p-utp) | [](https://travis-ci.com/libp2p/js-libp2p-utp) | [](https://codecov.io/gh/libp2p/js-libp2p-utp) | [Vasco Santos](mailto:santos.vasco10@gmail.com) |
|
||||
| [`libp2p-webrtc-direct`](//github.com/libp2p/js-libp2p-webrtc-direct) | [](//github.com/libp2p/js-libp2p-webrtc-direct/releases) | [](https://david-dm.org/libp2p/js-libp2p-webrtc-direct) | [](https://travis-ci.com/libp2p/js-libp2p-webrtc-direct) | [](https://codecov.io/gh/libp2p/js-libp2p-webrtc-direct) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
|
||||
| [`libp2p-webrtc-star`](//github.com/libp2p/js-libp2p-webrtc-star) | [](//github.com/libp2p/js-libp2p-webrtc-star/releases) | [](https://david-dm.org/libp2p/js-libp2p-webrtc-star) | [](https://travis-ci.com/libp2p/js-libp2p-webrtc-star) | [](https://codecov.io/gh/libp2p/js-libp2p-webrtc-star) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
|
||||
| [`libp2p-websockets`](//github.com/libp2p/js-libp2p-websockets) | [](//github.com/libp2p/js-libp2p-websockets/releases) | [](https://david-dm.org/libp2p/js-libp2p-websockets) | [](https://travis-ci.com/libp2p/js-libp2p-websockets) | [](https://codecov.io/gh/libp2p/js-libp2p-websockets) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| [`libp2p-websocket-star`](//github.com/libp2p/js-libp2p-websocket-star) | [](//github.com/libp2p/js-libp2p-websocket-star/releases) | [](https://david-dm.org/libp2p/js-libp2p-websocket-star) | [](https://travis-ci.com/libp2p/js-libp2p-websocket-star) | [](https://codecov.io/gh/libp2p/js-libp2p-websocket-star) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| **secure channels** |
|
||||
| [`libp2p-secio`](//github.com/libp2p/js-libp2p-secio) | [](//github.com/libp2p/js-libp2p-secio/releases) | [](https://david-dm.org/libp2p/js-libp2p-secio) | [](https://travis-ci.com/libp2p/js-libp2p-secio) | [](https://codecov.io/gh/libp2p/js-libp2p-secio) | [Friedel Ziegelmayer](mailto:dignifiedquire@gmail.com) |
|
||||
| **stream multiplexers** |
|
||||
| [`libp2p-mplex`](//github.com/libp2p/js-libp2p-mplex) | [](//github.com/libp2p/js-libp2p-mplex/releases) | [](https://david-dm.org/libp2p/js-libp2p-mplex) | [](https://travis-ci.com/libp2p/js-libp2p-mplex) | [](https://codecov.io/gh/libp2p/js-libp2p-mplex) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
|
||||
| [`libp2p-spdy`](//github.com/libp2p/js-libp2p-spdy) | [](//github.com/libp2p/js-libp2p-spdy/releases) | [](https://david-dm.org/libp2p/js-libp2p-spdy) | [](https://travis-ci.com/libp2p/js-libp2p-spdy) | [](https://codecov.io/gh/libp2p/js-libp2p-spdy) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| **peer discovery** |
|
||||
| [`libp2p-bootstrap`](//github.com/libp2p/js-libp2p-bootstrap) | [](//github.com/libp2p/js-libp2p-bootstrap/releases) | [](https://david-dm.org/libp2p/js-libp2p-bootstrap) | [](https://travis-ci.com/libp2p/js-libp2p-bootstrap) | [](https://codecov.io/gh/libp2p/js-libp2p-bootstrap) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
|
||||
| [`libp2p-kad-dht`](//github.com/libp2p/js-libp2p-kad-dht) | [](//github.com/libp2p/js-libp2p-kad-dht/releases) | [](https://david-dm.org/libp2p/js-libp2p-kad-dht) | [](https://travis-ci.com/libp2p/js-libp2p-kad-dht) | [](https://codecov.io/gh/libp2p/js-libp2p-kad-dht) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
|
||||
| [`libp2p-mdns`](//github.com/libp2p/js-libp2p-mdns) | [](//github.com/libp2p/js-libp2p-mdns/releases) | [](https://david-dm.org/libp2p/js-libp2p-mdns) | [](https://travis-ci.com/libp2p/js-libp2p-mdns) | [](https://codecov.io/gh/libp2p/js-libp2p-mdns) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| [`libp2p-rendezvous`](//github.com/libp2p/js-libp2p-rendezvous) | [](//github.com/libp2p/js-libp2p-rendezvous/releases) | [](https://david-dm.org/libp2p/js-libp2p-rendezvous) | [](https://travis-ci.com/libp2p/js-libp2p-rendezvous) | [](https://codecov.io/gh/libp2p/js-libp2p-rendezvous) | [Vasco Santos](mailto:santos.vasco10@gmail.com) |
|
||||
| [`libp2p-webrtc-star`](//github.com/libp2p/js-libp2p-webrtc-star) | [](//github.com/libp2p/js-libp2p-webrtc-star/releases) | [](https://david-dm.org/libp2p/js-libp2p-webrtc-star) | [](https://travis-ci.com/libp2p/js-libp2p-webrtc-star) | [](https://codecov.io/gh/libp2p/js-libp2p-webrtc-star) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
|
||||
| [`libp2p-websocket-star`](//github.com/libp2p/js-libp2p-websocket-star) | [](//github.com/libp2p/js-libp2p-websocket-star/releases) | [](https://david-dm.org/libp2p/js-libp2p-websocket-star) | [](https://travis-ci.com/libp2p/js-libp2p-websocket-star) | [](https://codecov.io/gh/libp2p/js-libp2p-websocket-star) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| **content routing** |
|
||||
| [`libp2p-delegated-content-routing`](//github.com/libp2p/js-libp2p-delegated-content-routing) | [](//github.com/libp2p/js-libp2p-delegated-content-routing/releases) | [](https://david-dm.org/libp2p/js-libp2p-delegated-content-routing) | [](https://travis-ci.com/libp2p/js-libp2p-delegated-content-routing) | [](https://codecov.io/gh/libp2p/js-libp2p-delegated-content-routing) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| [`libp2p-kad-dht`](//github.com/libp2p/js-libp2p-kad-dht) | [](//github.com/libp2p/js-libp2p-kad-dht/releases) | [](https://david-dm.org/libp2p/js-libp2p-kad-dht) | [](https://travis-ci.com/libp2p/js-libp2p-kad-dht) | [](https://codecov.io/gh/libp2p/js-libp2p-kad-dht) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
|
||||
| **peer routing** |
|
||||
| [`libp2p-delegated-peer-routing`](//github.com/libp2p/js-libp2p-delegated-peer-routing) | [](//github.com/libp2p/js-libp2p-delegated-peer-routing/releases) | [](https://david-dm.org/libp2p/js-libp2p-delegated-peer-routing) | [](https://travis-ci.com/libp2p/js-libp2p-delegated-peer-routing) | [](https://codecov.io/gh/libp2p/js-libp2p-delegated-peer-routing) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| [`libp2p-kad-dht`](//github.com/libp2p/js-libp2p-kad-dht) | [](//github.com/libp2p/js-libp2p-kad-dht/releases) | [](https://david-dm.org/libp2p/js-libp2p-kad-dht) | [](https://travis-ci.com/libp2p/js-libp2p-kad-dht) | [](https://codecov.io/gh/libp2p/js-libp2p-kad-dht) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
|
||||
| **utilities** |
|
||||
| [`libp2p-crypto`](//github.com/libp2p/js-libp2p-crypto) | [](//github.com/libp2p/js-libp2p-crypto/releases) | [](https://david-dm.org/libp2p/js-libp2p-crypto) | [](https://travis-ci.com/libp2p/js-libp2p-crypto) | [](https://codecov.io/gh/libp2p/js-libp2p-crypto) | [Jacob Heun](mailto:jacobheun@gmail.com) |
|
||||
| [`libp2p-crypto-secp256k1`](//github.com/libp2p/js-libp2p-crypto-secp256k1) | [](//github.com/libp2p/js-libp2p-crypto-secp256k1/releases) | [](https://david-dm.org/libp2p/js-libp2p-crypto-secp256k1) | [](https://travis-ci.com/libp2p/js-libp2p-crypto-secp256k1) | [](https://codecov.io/gh/libp2p/js-libp2p-crypto-secp256k1) | [Friedel Ziegelmayer](mailto:dignifiedquire@gmail.com) |
|
||||
| **data types** |
|
||||
| [`peer-id`](//github.com/libp2p/js-peer-id) | [](//github.com/libp2p/js-peer-id/releases) | [](https://david-dm.org/libp2p/js-peer-id) | [](https://travis-ci.com/libp2p/js-peer-id) | [](https://codecov.io/gh/libp2p/js-peer-id) | [Vasco Santos](mailto:santos.vasco10@gmail.com) |
|
||||
| [`peer-info`](//github.com/libp2p/js-peer-info) | [](//github.com/libp2p/js-peer-info/releases) | [](https://david-dm.org/libp2p/js-peer-info) | [](https://travis-ci.com/libp2p/js-peer-info) | [](https://codecov.io/gh/libp2p/js-peer-info) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
|
||||
| **pubsub** |
|
||||
| [`libp2p-pubsub`](//github.com/libp2p/js-libp2p-pubsub) | [](//github.com/libp2p/js-libp2p-pubsub/releases) | [](https://david-dm.org/libp2p/js-libp2p-pubsub) | [](https://travis-ci.com/libp2p/js-libp2p-pubsub) | [](https://codecov.io/gh/libp2p/js-libp2p-pubsub) | [Vasco Santos](mailto:santos.vasco10@gmail.com) |
|
||||
| [`libp2p-floodsub`](//github.com/libp2p/js-libp2p-floodsub) | [](//github.com/libp2p/js-libp2p-floodsub/releases) | [](https://david-dm.org/libp2p/js-libp2p-floodsub) | [](https://travis-ci.com/libp2p/js-libp2p-floodsub) | [](https://codecov.io/gh/libp2p/js-libp2p-floodsub) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
|
||||
| [`libp2p-gossipsub`](//github.com/ChainSafe/gossipsub-js) | [](//github.com/ChainSafe/gossipsub-js/releases) | [](https://david-dm.org/ChainSafe/gossipsub-js) | [](https://travis-ci.com/ChainSafe/gossipsub-js) | [](https://codecov.io/gh/ChainSafe/gossipsub-js) | [Cayman Nava](mailto:caymannava@gmail.com) |
|
||||
| **extensions** |
|
||||
| [`libp2p-nat-mgnr`](//github.com/libp2p/js-libp2p-nat-mgnr) | [](//github.com/libp2p/js-libp2p-nat-mgnr/releases) | [](https://david-dm.org/libp2p/js-libp2p-nat-mgnr) | [](https://travis-ci.com/libp2p/js-libp2p-nat-mgnr) | [](https://codecov.io/gh/libp2p/js-libp2p-nat-mgnr) | N/A |
|
||||
| [`libp2p-utils`](//github.com/libp2p/js-libp2p-utils) | [](//github.com/libp2p/js-libp2p-utils/releases) | [](https://david-dm.org/libp2p/js-libp2p-utils) | [](https://travis-ci.com/libp2p/js-libp2p-utils) | [](https://codecov.io/gh/libp2p/js-libp2p-utils) | [Vasco Santos](mailto:santos.vasco10@gmail.com) |
|
||||
|
||||
## Contribute
|
||||
|
||||
|
@ -50,8 +50,8 @@ Would you like to contribute to the libp2p project and don't know how? Well, the
|
||||
- Check the issues with the `help wanted` label in the [libp2p repo](https://github.com/libp2p/js-libp2p/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22)
|
||||
- Join an IPFS All Hands, introduce yourself and let us know where you would like to contribute - https://github.com/ipfs/team-mgmt#all-hands-call
|
||||
- Hack with IPFS and show us what you made! The All Hands call is also the perfect venue for demos, join in and show us what you built
|
||||
- Join the discussion at http://discuss.ipfs.io/ and help users finding their answers.
|
||||
- Join the [⚡️ⒿⓈ Core Dev Team Weekly Sync 🙌🏽 ](https://github.com/ipfs/team-mgmt/issues/650) and be part of the Sprint action!
|
||||
- Join the discussion at http://discuss.libp2p.io/ and help users finding their answers.
|
||||
- Join the [⚡️libp2p Weekly Sync 🙌🏽](https://github.com/libp2p/team-mgmt/issues/16) and be part of the Sprint action!
|
||||
|
||||
# ⁉️ Do you have questions?
|
||||
|
||||
|
772
doc/API.md
Normal file
772
doc/API.md
Normal file
@ -0,0 +1,772 @@
|
||||
# API
|
||||
|
||||
* [Static Functions](#static-functions)
|
||||
* [`create`](#create)
|
||||
* [Instance Methods](#instance-methods)
|
||||
* [`start`](#start)
|
||||
* [`stop`](#stop)
|
||||
* [`dial`](#dial)
|
||||
* [`dialProtocol`](#dialprotocol)
|
||||
* [`hangUp`](#hangup)
|
||||
* [`handle`](#handle)
|
||||
* [`unhandle`](#unhandle)
|
||||
* [`ping`](#ping)
|
||||
* [`peerRouting.findPeer`](#peerroutingfindpeer)
|
||||
* [`contentRouting.findProviders`](#contentroutingfindproviders)
|
||||
* [`contentRouting.provide`](#contentroutingprovide)
|
||||
* [`contentRouting.put`](#contentroutingput)
|
||||
* [`contentRouting.get`](#contentroutingget)
|
||||
* [`contentRouting.getMany`](#contentroutinggetmany)
|
||||
* [`pubsub.getSubscribers`](#pubsubgetsubscribers)
|
||||
* [`pubsub.getTopics`](#pubsubgettopics)
|
||||
* [`pubsub.publish`](#pubsubpublish)
|
||||
* [`pubsub.subscribe`](#pubsubsubscribe)
|
||||
* [`pubsub.unsubscribe`](#pubsubunsubscribe)
|
||||
* [`connectionManager.setPeerValue`](#connectionmanagersetpeervalue)
|
||||
* [`metrics.global`](#metricsglobal)
|
||||
* [`metrics.peers`](#metricspeers)
|
||||
* [`metrics.protocols`](#metricsprotocols)
|
||||
* [`metrics.forPeer`](#metricsforpeer)
|
||||
* [`metrics.forProtocol`](#metricsforprotocol)
|
||||
* [Types](#types)
|
||||
* [`Stats`](#stats)
|
||||
|
||||
## Static Functions
|
||||
|
||||
### create
|
||||
|
||||
Creates an instance of Libp2p.
|
||||
|
||||
`create(options)`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| options | `Object` | libp2p options |
|
||||
| options.modules | `Array<Object>` | libp2p modules to use |
|
||||
| [options.config] | `Object` | libp2p modules configuration and core configuration |
|
||||
| [options.datastore] | `Object` | must implement [ipfs/interface-datastore](https://github.com/ipfs/interface-datastore) (in memory datastore will be used if not provided) |
|
||||
| [options.peerInfo] | [PeerInfo](https://github.com/libp2p/js-peer-info) | peerInfo instance (it will be created if not provided) |
|
||||
|
||||
For Libp2p configurations and modules details read the [Configuration Document](./CONFIGURATION.md).
|
||||
|
||||
#### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `Promise<Libp2p>` | Promise resolves with the Libp2p instance |
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
const Libp2p = require('libp2p')
|
||||
|
||||
// specify options
|
||||
const options = {}
|
||||
|
||||
// create libp2p
|
||||
const libp2p = await Libp2p.create(options)
|
||||
```
|
||||
|
||||
Note: The `PeerInfo` option is not required and will be generated if it is not provided.
|
||||
|
||||
<details><summary>Alternative</summary>
|
||||
As an alternative, it is possible to create a Libp2p instance with the constructor:
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
const Libp2p = require('libp2p')
|
||||
|
||||
// specify options
|
||||
const options = {}
|
||||
|
||||
// create libp2p
|
||||
const libp2p = new Libp2p(options)
|
||||
```
|
||||
|
||||
Required keys in the `options` object:
|
||||
|
||||
- `peerInfo`: instance of [PeerInfo][] that contains the [PeerId][], Keys and [multiaddrs][multiaddr] of the libp2p Node (optional when using `.create`).
|
||||
- `modules.transport`: An array that must include at least 1 compliant transport. See [modules that implement the transport interface](https://github.com/libp2p/js-interfaces/tree/master/src/transport#modules-that-implement-the-interface).
|
||||
|
||||
</details>
|
||||
|
||||
Once you have a libp2p instance, you are able to listen to several events it emits, so that you can be noticed of relevant network events.
|
||||
|
||||
<details><summary>Events</summary>
|
||||
|
||||
#### An error has occurred
|
||||
|
||||
`libp2p.on('error', (err) => {})`
|
||||
|
||||
- `err`: instance of `Error`
|
||||
|
||||
#### A peer has been discovered
|
||||
|
||||
`libp2p.on('peer:discovery', (peer) => {})`
|
||||
|
||||
If `autoDial` option is `true`, applications should **not** attempt to connect to the peer
|
||||
unless they are performing a specific action. See [peer discovery and auto dial](./PEER_DISCOVERY.md) for more information.
|
||||
|
||||
- `peer`: instance of [PeerInfo][https://github.com/libp2p/js-peer-info]
|
||||
|
||||
#### We have a new connection to a peer
|
||||
|
||||
`libp2p.on('peer:connect', (peer) => {})`
|
||||
|
||||
- `peer`: instance of [PeerInfo][https://github.com/libp2p/js-peer-info]
|
||||
|
||||
#### We have closed a connection to a peer
|
||||
|
||||
`libp2p.on('peer:disconnect', (peer) => {})`
|
||||
|
||||
- `peer`: instance of [PeerInfo][https://github.com/libp2p/js-peer-info]
|
||||
|
||||
</details>
|
||||
|
||||
## Libp2p Instance Methods
|
||||
|
||||
### start
|
||||
|
||||
Starts the libp2p node.
|
||||
|
||||
`libp2p.start()`
|
||||
|
||||
#### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `Promise` | Promise resolves when the node is ready |
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
const Libp2p = require('libp2p')
|
||||
|
||||
// ...
|
||||
|
||||
const libp2p = await Libp2p.create(options)
|
||||
|
||||
// start libp2p
|
||||
await libp2p.start()
|
||||
```
|
||||
|
||||
### stop
|
||||
|
||||
Stops the libp2p node.
|
||||
|
||||
`libp2p.stop()`
|
||||
|
||||
#### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `Promise` | Promise resolves when the node is fully stopped |
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
const Libp2p = require('libp2p')
|
||||
|
||||
// ...
|
||||
const libp2p = await Libp2p.create(options)
|
||||
// ...
|
||||
|
||||
// stop libp2p
|
||||
await libp2p.stop()
|
||||
```
|
||||
|
||||
### dial
|
||||
|
||||
Dials to another peer in the network and establishes the connection.
|
||||
|
||||
`dial(peer, options)`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| peer | [PeerInfo](https://github.com/libp2p/js-peer-info), [PeerId](https://github.com/libp2p/js-peer-id), [multiaddr](https://github.com/multiformats/js-multiaddr), `string` | peer to dial |
|
||||
| [options] | `Object` | dial options |
|
||||
| [options.signal] | [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) | An `AbortSignal` instance obtained from an [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) that can be used to abort the connection before it completes |
|
||||
|
||||
#### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `Promise<Connection>` | Promise resolves with the [Connection](https://github.com/libp2p/js-interfaces/tree/master/src/connection) instance |
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
// ...
|
||||
const conn = await libp2p.dial(remotePeerInfo)
|
||||
|
||||
// create a new stream within the connection
|
||||
const { stream, protocol } = await conn.newStream(['/echo/1.1.0', '/echo/1.0.0'])
|
||||
|
||||
// protocol negotiated: 'echo/1.0.0' means that the other party only supports the older version
|
||||
|
||||
// ...
|
||||
await conn.close()
|
||||
```
|
||||
|
||||
### dialProtocol
|
||||
|
||||
Dials to another peer in the network and selects a protocol to communicate with that peer. The stream between both parties is returned, together with the negotiated protocol.
|
||||
|
||||
`dialProtocol(peer, protocols, options)`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| peer | [PeerInfo](https://github.com/libp2p/js-peer-info), [PeerId](https://github.com/libp2p/js-peer-id), [multiaddr](https://github.com/multiformats/js-multiaddr), `string` | peer to dial |
|
||||
| protocols | `String|Array<String>` | A list of protocols (or single protocol) to negotiate with. Protocols are attempted in order until a match is made. (e.g '/ipfs/bitswap/1.1.0') |
|
||||
| [options] | `Object` | dial options |
|
||||
| [options.signal] | [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) | An `AbortSignal` instance obtained from an [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) that can be used to abort the connection before it completes |
|
||||
|
||||
#### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `Promise<{ stream:*, protocol:string }>` | Promise resolves with a [duplex stream](https://gist.github.com/alanshaw/591dc7dd54e4f99338a347ef568d6ee9#duplex-it) and the protocol used |
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
// ...
|
||||
const pipe = require('it-pipe')
|
||||
|
||||
const { stream, protocol } = await libp2p.dialProtocol(remotePeerInfo, protocols)
|
||||
|
||||
// Use this new stream like any other duplex stream
|
||||
pipe([1, 2, 3], stream, consume)
|
||||
```
|
||||
|
||||
### hangUp
|
||||
|
||||
Attempts to gracefully close an open connection to the given peer. If the connection is not closed in the grace period, it will be forcefully closed.
|
||||
|
||||
`hangUp(peer)`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| peer | [PeerInfo](https://github.com/libp2p/js-peer-info), [PeerId](https://github.com/libp2p/js-peer-id), [multiaddr](https://github.com/multiformats/js-multiaddr), `string` | peer to hang up |
|
||||
|
||||
#### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `Promise<void>` | Promise resolves once connection closes |
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
// ...
|
||||
await libp2p.hangUp(remotePeerInfo)
|
||||
```
|
||||
|
||||
### handle
|
||||
|
||||
Sets up [multistream-select routing](https://github.com/multiformats/multistream-select) of protocols to their application handlers. Whenever a stream is opened on one of the provided protocols, the handler will be called. `handle` must be called in order to register a handler and support for a given protocol. This also informs other peers of the protocols you support.
|
||||
|
||||
`libp2p.handle(protocols, handler)`
|
||||
|
||||
In the event of a new handler for the same protocol being added, the first one is discarded.
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| protocols | `Array<String>|String` | protocols to register |
|
||||
| handler | `function({ connection:*, stream:*, protocol:string })` | handler to call |
|
||||
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
// ...
|
||||
const handler = ({ connection, stream, protocol }) => {
|
||||
// use stream or connection according to the needs
|
||||
}
|
||||
|
||||
libp2p.handle('/echo/1.0.0', handler)
|
||||
```
|
||||
|
||||
### unhandle
|
||||
|
||||
Unregisters all handlers with the given protocols
|
||||
|
||||
`libp2p.unhandle(protocols)`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| protocols | `Array<String>|String` | protocols to unregister |
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
// ...
|
||||
libp2p.unhandle(['/echo/1.0.0'])
|
||||
```
|
||||
|
||||
### ping
|
||||
|
||||
Pings a given peer and get the operation's latency.
|
||||
|
||||
`libp2p.ping(peer)`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| peer | `PeerInfo|PeerId|Multiaddr|string` | peer to ping |
|
||||
|
||||
#### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `Promise<number>` | Latency of the operation in ms |
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
// ...
|
||||
const latency = await libp2p.ping(otherPeerId)
|
||||
```
|
||||
|
||||
### peerRouting.findPeer
|
||||
|
||||
Iterates over all peer routers in series to find the given peer. If the DHT is enabled, it will be tried first.
|
||||
|
||||
`libp2p.peerRouting.findPeer(peerId, options)`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| peerId | [`PeerId`](https://github.com/libp2p/js-peer-id) | ID of the peer to find |
|
||||
| options | `Object` | operation options |
|
||||
| options.timeout | `number` | maximum time the query should run |
|
||||
|
||||
#### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `Promise<PeerInfo>` | Peer info of a known peer |
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
// ...
|
||||
const peerInfo = await libp2p.peerRouting.findPeer(peerId, options)
|
||||
```
|
||||
|
||||
### contentRouting.findProviders
|
||||
|
||||
Iterates over all content routers in series to find providers of the given key.
|
||||
Once a content router succeeds, the iteration will stop. If the DHT is enabled, it will be queried first.
|
||||
|
||||
`libp2p.contentRouting.findProviders(cid, options)`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| cid | [`CID`](https://github.com/multiformats/js-cid) | cid to find |
|
||||
| options | `Object` | operation options |
|
||||
| options.timeout | `number` | maximum time the query should run |
|
||||
| options.maxNumProviders | `number` | maximum number of providers to find |
|
||||
|
||||
#### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `AsyncIterator<PeerInfo>` | Async iterator for [`PeerInfo`](https://github.com/libp2p/js-peer-info) |
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
// Iterate over the providers found for the given cid
|
||||
for await (const provider of libp2p.contentRouting.findProviders(cid)) {
|
||||
console.log(provider)
|
||||
}
|
||||
```
|
||||
|
||||
### contentRouting.provide
|
||||
|
||||
Iterates over all content routers in parallel, in order to notify it is a provider of the given key.
|
||||
|
||||
`libp2p.contentRouting.provide(cid)`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| cid | [`CID`](https://github.com/multiformats/js-cid) | cid to provide |
|
||||
|
||||
#### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `Promise<void>` | Promise resolves once notifications are sent |
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
// ...
|
||||
await libp2p.contentRouting.provide(cid)
|
||||
```
|
||||
|
||||
### contentRouting.put
|
||||
|
||||
Writes a value to a key in the DHT.
|
||||
|
||||
`libp2p.contentRouting.put(key, value, options)`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| key | `String` | key to add to the dht |
|
||||
| value | `Buffer` | value to add to the dht |
|
||||
| [options] | `Object` | put options |
|
||||
| [options.minPeers] | `number` | minimum number of peers required to successfully put (default: closestPeers.length) |
|
||||
|
||||
#### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `Promise<void>` | Promise resolves once value is stored |
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
// ...
|
||||
const key = '/key'
|
||||
const value = Buffer.from('oh hello there')
|
||||
|
||||
await libp2p.contentRouting.put(key, value)
|
||||
```
|
||||
|
||||
### contentRouting.get
|
||||
|
||||
Queries the DHT for a value stored for a given key.
|
||||
|
||||
`libp2p.contentRouting.get(key, options)`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| key | `String` | key to get from the dht |
|
||||
| [options] | `Object` | get options |
|
||||
| [options.timeout] | `number` | maximum time the query should run |
|
||||
|
||||
#### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `Promise<Buffer>` | Value obtained from the DHT |
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
// ...
|
||||
|
||||
const key = '/key'
|
||||
const value = await libp2p.contentRouting.get(key)
|
||||
```
|
||||
|
||||
### contentRouting.getMany
|
||||
|
||||
Queries the DHT for the n values stored for the given key (without sorting).
|
||||
|
||||
`libp2p.contentRouting.getMany(key, nvals, options)`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| key | `String` | key to get from the dht |
|
||||
| nvals | `number` | number of values aimed |
|
||||
| [options] | `Object` | get options |
|
||||
| [options.timeout] | `number` | maximum time the query should run |
|
||||
|
||||
#### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `Promise<Array<{from: PeerId, val: Buffer}>>` | Array of records obtained from the DHT |
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
// ...
|
||||
|
||||
const key = '/key'
|
||||
const { from, val } = await libp2p.contentRouting.get(key)
|
||||
```
|
||||
|
||||
### pubsub.getSubscribers
|
||||
|
||||
Gets a list of the peer-ids that are subscribed to one topic.
|
||||
|
||||
`libp2p.pubsub.getSubscribers(topic)`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| topic | `string` | topic to publish |
|
||||
|
||||
#### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `Array<String>` | peer-id subscribed to the topic |
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
const peerIds = libp2p.pubsub.getSubscribers(topic)
|
||||
```
|
||||
|
||||
### pubsub.getTopics
|
||||
|
||||
Gets a list of topics the node is subscribed to.
|
||||
|
||||
`libp2p.pubsub.getTopics()`
|
||||
|
||||
#### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `Array<String>` | topics the node is subscribed to |
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
const topics = libp2p.pubsub.getTopics()
|
||||
```
|
||||
|
||||
### pubsub.publish
|
||||
|
||||
Publishes messages to the given topics.
|
||||
|
||||
`libp2p.pubsub.publish(topic, data)`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| topic | `string` | topic to publish |
|
||||
| data | `Buffer` | data to publish |
|
||||
|
||||
#### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `Promise` | publish success |
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
const topic = 'topic'
|
||||
const data = Buffer.from('data')
|
||||
|
||||
await libp2p.pubsub.publish(topic, data)
|
||||
```
|
||||
|
||||
### pubsub.subscribe
|
||||
|
||||
Subscribes the given handler to a pubsub topic.
|
||||
|
||||
`libp2p.pubsub.subscribe(topic, handler)`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| topic | `string` | topic to subscribe |
|
||||
| handler | `function({ from: String, data: Buffer, seqno: Buffer, topicIDs: Array<String>, signature: Buffer, key: Buffer })` | handler for new data on topic |
|
||||
|
||||
#### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `void` | |
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
const topic = 'topic'
|
||||
const handler = (msg) => {
|
||||
// msg.data - pubsub data received
|
||||
}
|
||||
|
||||
libp2p.pubsub.subscribe(topic, handler)
|
||||
```
|
||||
|
||||
### pubsub.unsubscribe
|
||||
|
||||
Unsubscribes the given handler from a pubsub topic. If no handler is provided, all handlers for the topic are removed.
|
||||
|
||||
`libp2p.pubsub.unsubscribe(topic, handler)`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| topic | `string` | topic to unsubscribe |
|
||||
| handler | `function(<Object>)` | handler subscribed |
|
||||
|
||||
#### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `void` | |
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
const topic = 'topic'
|
||||
const handler = (msg) => {
|
||||
// msg.data - pubsub data received
|
||||
}
|
||||
|
||||
libp2p.pubsub.unsubscribe(topic, handler)
|
||||
```
|
||||
|
||||
### connectionManager.setPeerValue
|
||||
|
||||
Enables users to change the value of certain peers in a range of 0 to 1. Peers with the lowest values will have their Connections pruned first, if any Connection Manager limits are exceeded. See [./CONFIGURATION.md#configuring-connection-manager](./CONFIGURATION.md#configuring-connection-manager) for details on how to configure these limits.
|
||||
|
||||
`libp2p.connectionManager.setPeerValue(peerId, value)`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| peerId | `PeerId` | The peer to set the value for |
|
||||
| value | `number` | The value of the peer from 0 to 1 |
|
||||
|
||||
#### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `void` | |
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
libp2p.connectionManager.setPeerValue(highPriorityPeerId, 1)
|
||||
libp2p.connectionManager.setPeerValue(lowPriorityPeerId, 0)
|
||||
```
|
||||
|
||||
### metrics.global
|
||||
|
||||
A [`Stats`](#stats) object of tracking the global bandwidth of the libp2p node.
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
const peerIdStrings = libp2p.metrics.peers
|
||||
```
|
||||
|
||||
### metrics.peers
|
||||
|
||||
An array of `PeerId` strings of each peer currently being tracked.
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
const peerIdStrings = libp2p.metrics.peers
|
||||
```
|
||||
|
||||
### metrics.protocols
|
||||
|
||||
An array of protocol strings that are currently being tracked.
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
const protocols = libp2p.metrics.protocols
|
||||
```
|
||||
|
||||
### metrics.forPeer
|
||||
|
||||
Returns the [`Stats`](#stats) object for a given `PeerId` if it is being tracked.
|
||||
|
||||
`libp2p.metrics.forPeer(peerId)`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| peerId | `PeerId` | The peer to get stats for |
|
||||
|
||||
#### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| [`Stats`](#stats) | The bandwidth stats of the peer |
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
const peerStats = libp2p.metrics.forPeer(peerInfo)
|
||||
console.log(peerStats.toJSON())
|
||||
```
|
||||
|
||||
### metrics.forProtocol
|
||||
|
||||
Returns the [`Stats`](#stats) object for a given protocol if it is being tracked.
|
||||
|
||||
`libp2p.metrics.forProtocol(protocol)`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| protocol | `string` | The protocol to get stats for |
|
||||
|
||||
#### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| [`Stats`](#stats) | The bandwidth stats of the protocol across all peers |
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
const peerStats = libp2p.metrics.forProtocol('/meshsub/1.0.0')
|
||||
console.log(peerStats.toJSON())
|
||||
```
|
||||
|
||||
## Types
|
||||
|
||||
### Stats
|
||||
|
||||
- `Stats`
|
||||
- `toJSON<function()>`: Returns a JSON snapshot of the stats.
|
||||
- `dataReceived<string>`: The stringified value of total incoming data for this stat.
|
||||
- `dataSent<string>`: The stringified value of total outgoing data for this stat.
|
||||
- `movingAverages<object>`: The properties are dependent on the configuration of the moving averages interval. Defaults are listed here.
|
||||
- `['60000']<Number>`: The calculated moving average at a 1 minute interval.
|
||||
- `['300000']<Number>`: The calculated moving average at a 5 minute interval.
|
||||
- `['900000']<Number>`: The calculated moving average at a 15 minute interval.
|
||||
- `snapshot<object>`: A getter that returns a clone of the raw stats.
|
||||
- `dataReceived<BigNumber>`: A [`BigNumber`](https://github.com/MikeMcl/bignumber.js/) of the amount of incoming data
|
||||
- `dataSent<BigNumber>`: A [`BigNumber`](https://github.com/MikeMcl/bignumber.js/) of the amount of outgoing data
|
||||
- `movingAverages<object>`: A getter that returns a clone of the raw [moving averages](https://www.npmjs.com/package/moving-averages) stats. **Note**: The properties of this are dependent on configuration. The defaults are shown here.
|
||||
- `['60000']<MovingAverage>`: The [MovingAverage](https://www.npmjs.com/package/moving-averages) at a 1 minute interval.
|
||||
- `['300000']<MovingAverage>`: The [MovingAverage](https://www.npmjs.com/package/moving-averages) at a 5 minute interval.
|
||||
- `['900000']<MovingAverage>`: The [MovingAverage](https://www.npmjs.com/package/moving-averages) at a 15 minute interval.
|
488
doc/CONFIGURATION.md
Normal file
488
doc/CONFIGURATION.md
Normal file
@ -0,0 +1,488 @@
|
||||
# Configuration
|
||||
|
||||
- [Configuration](#configuration)
|
||||
- [Overview](#overview)
|
||||
- [Modules](#modules)
|
||||
- [Transport](#transport)
|
||||
- [Stream Multiplexing](#stream-multiplexing)
|
||||
- [Connection Encryption](#connection-encryption)
|
||||
- [Peer Discovery](#peer-discovery)
|
||||
- [Content Routing](#content-routing)
|
||||
- [Peer Routing](#peer-routing)
|
||||
- [DHT](#dht)
|
||||
- [Pubsub](#pubsub)
|
||||
- [Customizing libp2p](#customizing-libp2p)
|
||||
- [Examples](#examples)
|
||||
- [Basic setup](#basic-setup)
|
||||
- [Customizing Peer Discovery](#customizing-peer-discovery)
|
||||
- [Setup webrtc transport and discovery](#setup-webrtc-transport-and-discovery)
|
||||
- [Customizing Pubsub](#customizing-pubsub)
|
||||
- [Customizing DHT](#customizing-dht)
|
||||
- [Setup with Content and Peer Routing](#setup-with-content-and-peer-routing)
|
||||
- [Setup with Relay](#setup-with-relay)
|
||||
- [Configuring Connection Manager](#configuring-connection-manager)
|
||||
- [Configuring Metrics](#configuring-metrics)
|
||||
- [Configuration examples](#configuration-examples)
|
||||
|
||||
## Overview
|
||||
|
||||
libp2p is a modular networking stack. It's designed to be able to suit a variety of project needs. The configuration of libp2p is a key part of its structure. It enables you to bring exactly what you need, and only what you need. This document is a guide on how to configure libp2p for your particular project. Check out the [Configuration examples](#configuration-examples) section if you're just looking to leverage an existing configuration.
|
||||
|
||||
Regardless of how you configure libp2p, the top level [API](./API.md) will always remain the same. **Note**: if some modules are not configured, like Content Routing, using those methods will throw errors.
|
||||
|
||||
## Modules
|
||||
|
||||
`js-libp2p` acts as the composer for this modular p2p networking stack using libp2p compatible modules as its subsystems. For getting an instance of `js-libp2p` compliant with all types of networking requirements, it is possible to specify the following subsystems:
|
||||
|
||||
- Transports
|
||||
- Multiplexers
|
||||
- Connection encryption mechanisms
|
||||
- Peer discovery protocols
|
||||
- Content routing protocols
|
||||
- Peer routing protocols
|
||||
- DHT implementation
|
||||
- Pubsub router
|
||||
|
||||
The libp2p ecosystem contains at least one module for each of these subsystems. The user should install and import the modules that are relevant for their requirements. Moreover, thanks to the existing interfaces it is easy to create a libp2p compatible module and use it.
|
||||
|
||||
After selecting the modules to use, it is also possible to configure each one according to your needs.
|
||||
|
||||
Bear in mind that only a **transport** and **connection encryption** are required, while all the other subsystems are optional.
|
||||
|
||||
### Transport
|
||||
|
||||
> In a p2p system, we need to interact with other peers in the network. Transports are used to establish connections between peers. The libp2p transports to use should be decided according to the environment where your node will live, as well as other requirements that you might have.
|
||||
|
||||
Some available transports are:
|
||||
|
||||
- [libp2p/js-libp2p-tcp](https://github.com/libp2p/js-libp2p-tcp)
|
||||
- [libp2p/js-libp2p-webrtc-star](https://github.com/libp2p/js-libp2p-webrtc-star)
|
||||
- [libp2p/js-libp2p-webrtc-direct](https://github.com/libp2p/js-libp2p-webrtc-direct)
|
||||
- [libp2p/js-libp2p-websockets](https://github.com/libp2p/js-libp2p-websockets)
|
||||
- [libp2p/js-libp2p-utp](https://github.com/libp2p/js-libp2p-utp) (Work in Progress)
|
||||
|
||||
You should take into consideration that `js-libp2p-tcp` and `js-libp2p-utp` are not available in a **browser** environment.
|
||||
|
||||
If none of the available transports fulfills your needs, you can create a libp2p compatible transport. A libp2p transport just needs to be compliant with the [Transport Interface](https://github.com/libp2p/js-interfaces/tree/master/src/transport).
|
||||
|
||||
If you want to know more about libp2p transports, you should read the following content:
|
||||
|
||||
- https://docs.libp2p.io/concepts/transport
|
||||
- https://github.com/libp2p/specs/tree/master/connections
|
||||
|
||||
### Stream Multiplexing
|
||||
|
||||
> Libp2p peers will need to communicate with each other through several protocols during their life. Stream multiplexing allows multiple independent logical streams to share a common underlying transport medium, instead of creating a new connection with the same peer per needed protocol.
|
||||
|
||||
Some available stream multiplexers are:
|
||||
|
||||
- [libp2p/js-libp2p-mplex](https://github.com/libp2p/js-libp2p-mplex)
|
||||
|
||||
If none of the available stream multiplexers fulfills your needs, you can create a libp2p compatible stream multiplexer. A libp2p multiplexer just needs to be compliant with the [Stream Muxer Interface](https://github.com/libp2p/js-interfaces/tree/master/src/stream-muxer).
|
||||
|
||||
If you want to know more about libp2p stream multiplexing, you should read the following content:
|
||||
|
||||
- https://docs.libp2p.io/concepts/stream-multiplexing
|
||||
- https://github.com/libp2p/specs/tree/master/connections
|
||||
- https://github.com/libp2p/specs/tree/master/mplex
|
||||
|
||||
### Connection Encryption
|
||||
|
||||
> A connection encryption mechanism must be used, in order to ensure all exchanged data between two peers is encrypted.
|
||||
|
||||
Some available connection encryption protocols:
|
||||
|
||||
- [libp2p/js-libp2p-secio](https://github.com/libp2p/js-libp2p-secio)
|
||||
|
||||
If none of the available connection encryption mechanisms fulfills your needs, you can create a libp2p compatible one. A libp2p connection encryption protocol just needs to be compliant with the [Crypto Interface](https://github.com/libp2p/js-interfaces/tree/master/src/crypto).
|
||||
|
||||
If you want to know more about libp2p connection encryption, you should read the following content:
|
||||
|
||||
- https://docs.libp2p.io/concepts/secure-comms
|
||||
- https://github.com/libp2p/specs/tree/master/connections
|
||||
|
||||
### Peer Discovery
|
||||
|
||||
> In a p2p network, peer discovery is critical to a functioning system.
|
||||
|
||||
Some available peer discovery modules are:
|
||||
|
||||
- [js-libp2p-mdns](https://github.com/libp2p/js-libp2p-mdns)
|
||||
- [js-libp2p-bootstrap](https://github.com/libp2p/js-libp2p-bootstrap)
|
||||
- [js-libp2p-kad-dht](https://github.com/libp2p/js-libp2p-kad-dht)
|
||||
- [js-libp2p-webrtc-star](https://github.com/libp2p/js-libp2p-webrtc-star)
|
||||
|
||||
**Note**: `peer-discovery` services within transports (such as `js-libp2p-webrtc-star`) are automatically gathered from the `transport`, via it's `discovery` property. As such, they do not need to be added in the discovery modules. However, these transports can also be configured and disabled as the other ones.
|
||||
|
||||
If none of the available peer discovery protocols fulfills your needs, you can create a libp2p compatible one. A libp2p peer discovery protocol just needs to be compliant with the [Peer Discovery Interface](https://github.com/libp2p/js-interfaces/tree/master/src/peer-discovery).
|
||||
|
||||
If you want to know more about libp2p peer discovery, you should read the following content:
|
||||
|
||||
- https://github.com/libp2p/specs/blob/master/discovery/mdns.md
|
||||
|
||||
### Content Routing
|
||||
|
||||
> Content routing provides a way to find where content lives in the network. It works in two steps: 1) Peers provide (announce) to the network that they are holders of specific content and 2) Peers issue queries to find where that content lives. A Content Routing mechanism could be as complex as a DHT or as simple as a registry somewhere in the network.
|
||||
|
||||
Some available content routing modules are:
|
||||
|
||||
- [js-libp2p-kad-dht](https://github.com/libp2p/js-libp2p-kad-dht)
|
||||
- [js-libp2p-delegated-peer-routing](https://github.com/libp2p/js-libp2p-delegated-peer-routing)
|
||||
|
||||
If none of the available content routing protocols fulfills your needs, you can create a libp2p compatible one. A libp2p content routing protocol just needs to be compliant with the [Content Routing Interface](https://github.com/libp2p/js-interfaces/tree/master/src/content-routing). **(WIP: This module is not yet implemented)**
|
||||
|
||||
If you want to know more about libp2p content routing, you should read the following content:
|
||||
|
||||
- https://docs.libp2p.io/concepts/content-routing
|
||||
|
||||
### Peer Routing
|
||||
|
||||
> Peer Routing offers a way to find other peers in the network by issuing queries using a Peer Routing algorithm, which may be iterative or recursive. If the algorithm is unable to find the target peer, it will return the peers that are "closest" to the target peer, using a distance metric defined by the algorithm.
|
||||
|
||||
Some available peer routing modules are:
|
||||
|
||||
- [js-libp2p-kad-dht](https://github.com/libp2p/js-libp2p-kad-dht)
|
||||
- [js-libp2p-delegated-peer-routing](https://github.com/libp2p/js-libp2p-delegated-peer-routing)
|
||||
|
||||
If none of the available peer routing protocols fulfills your needs, you can create a libp2p compatible one. A libp2p peer routing protocol just needs to be compliant with the [Peer Routing Interface](https://github.com/libp2p/js-interfaces/tree/master/src/peer-routing). **(WIP: This module is not yet implemented)**
|
||||
|
||||
If you want to know more about libp2p peer routing, you should read the following content:
|
||||
|
||||
- https://docs.libp2p.io/concepts/peer-routing
|
||||
|
||||
### DHT
|
||||
|
||||
> A DHT can provide content and peer routing capabilities in a p2p system, as well as peer discovery capabilities.
|
||||
|
||||
The DHT implementation currently available is [libp2p/js-libp2p-kad-dht](https://github.com/libp2p/js-libp2p-kad-dht). This implementation is largely based on the Kademlia whitepaper, augmented with notions from S/Kademlia, Coral and mainlineDHT.
|
||||
|
||||
If this DHT implementation does not fulfill your needs and you want to create or use your own implementation, please get in touch with us through a github issue. We plan to work on improving the ability to bring your own DHT in a future release.
|
||||
|
||||
If you want to know more about libp2p DHT, you should read the following content:
|
||||
|
||||
- https://docs.libp2p.io/concepts/protocols/#kad-dht
|
||||
- https://github.com/libp2p/specs/pull/108
|
||||
|
||||
### Pubsub
|
||||
|
||||
> Publish/Subscribe is a system where peers congregate around topics they are interested in. Peers interested in a topic are said to be subscribed to that topic and should receive the data published on it from other peers.
|
||||
|
||||
Some available pubsub routers are:
|
||||
|
||||
- [libp2p/js-libp2p-floodsub](https://github.com/libp2p/js-libp2p-floodsub)
|
||||
- [ChainSafe/gossipsub-js](https://github.com/ChainSafe/gossipsub-js)
|
||||
|
||||
If none of the available pubsub routers fulfills your needs, you can create a libp2p compatible one. A libp2p pubsub router just needs to be created on top of [libp2p/js-libp2p-pubsub](https://github.com/libp2p/js-libp2p-pubsub), which ensures `js-libp2p` API expectations.
|
||||
|
||||
If you want to know more about libp2p pubsub, you should read the following content:
|
||||
|
||||
- https://docs.libp2p.io/concepts/publish-subscribe
|
||||
- https://github.com/libp2p/specs/tree/master/pubsub
|
||||
|
||||
## Customizing libp2p
|
||||
|
||||
When [creating a libp2p node](./API.md#create), the modules needed should be specified as follows:
|
||||
|
||||
```js
|
||||
const modules = {
|
||||
transport: [],
|
||||
streamMuxer: [],
|
||||
connEncryption: [],
|
||||
contentRouting: [],
|
||||
peerRouting: [],
|
||||
peerDiscovery: [],
|
||||
dht: dhtImplementation,
|
||||
pubsub: pubsubImplementation
|
||||
}
|
||||
```
|
||||
|
||||
Moreover, the majority of the modules can be customized via option parameters. This way, it is also possible to provide this options through a `config` object. This config object should have the property name of each building block to configure, the same way as the modules specification.
|
||||
|
||||
Besides the `modules` and `config`, libp2p allows other internal options and configurations:
|
||||
- `datastore`: an instance of [ipfs/interface-datastore](https://github.com/ipfs/interface-datastore/) modules.
|
||||
- This is used in modules such as the DHT. If it is not provided, `js-libp2p` will use an in memory datastore.
|
||||
- `peerInfo`: a previously created instance of [libp2p/js-peer-info](https://github.com/libp2p/js-peer-info).
|
||||
- This is particularly useful if you want to reuse the same `peer-id`, as well as for modules like `libp2p-delegated-content-routing`, which need a `peer-id` in their instantiation.
|
||||
|
||||
### Examples
|
||||
|
||||
#### Basic setup
|
||||
|
||||
```js
|
||||
// Creating a libp2p node with:
|
||||
// transport: websockets + tcp
|
||||
// stream-muxing: mplex
|
||||
// crypto-channel: secio
|
||||
// discovery: multicast-dns
|
||||
// dht: kad-dht
|
||||
// pubsub: gossipsub
|
||||
|
||||
const Libp2p = require('libp2p')
|
||||
const TCP = require('libp2p-tcp')
|
||||
const WS = require('libp2p-websockets')
|
||||
const MPLEX = require('libp2p-mplex')
|
||||
const SECIO = require('libp2p-secio')
|
||||
const MulticastDNS = require('libp2p-mdns')
|
||||
const DHT = require('libp2p-kad-dht')
|
||||
const GossipSub = require('libp2p-gossipsub')
|
||||
|
||||
const node = await Libp2p.create({
|
||||
modules: {
|
||||
transport: [
|
||||
TCP,
|
||||
new WS() // It can take instances too!
|
||||
],
|
||||
streamMuxer: [MPLEX],
|
||||
connEncryption: [SECIO],
|
||||
peerDiscovery: [MulticastDNS],
|
||||
dht: DHT,
|
||||
pubsub: GossipSub
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### Customizing Peer Discovery
|
||||
|
||||
```js
|
||||
const Libp2p = require('libp2p')
|
||||
const TCP = require('libp2p-tcp')
|
||||
const MPLEX = require('libp2p-mplex')
|
||||
const SECIO = require('libp2p-secio')
|
||||
const MulticastDNS = require('libp2p-mdns')
|
||||
|
||||
const node = await Libp2p.create({
|
||||
modules: {
|
||||
transport: [TCP],
|
||||
streamMuxer: [MPLEX],
|
||||
connEncryption: [SECIO],
|
||||
peerDiscovery: [MulticastDNS]
|
||||
},
|
||||
config: {
|
||||
peerDiscovery: {
|
||||
autoDial: true, // Auto connect to discovered peers (limited by ConnectionManager minPeers)
|
||||
// The `tag` property will be searched when creating the instance of your Peer Discovery service.
|
||||
// The associated object, will be passed to the service when it is instantiated.
|
||||
[MulticastDNS.tag]: {
|
||||
interval: 1000,
|
||||
enabled: true
|
||||
}
|
||||
// .. other discovery module options.
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### Setup webrtc transport and discovery
|
||||
|
||||
```js
|
||||
|
||||
const Libp2p = require('libp2p')
|
||||
const WS = require('libp2p-websockets')
|
||||
const WebRTCStar = require('libp2p-webrtc-star')
|
||||
const MPLEX = require('libp2p-mplex')
|
||||
const SECIO = require('libp2p-secio')
|
||||
|
||||
const node = await Libp2p.create({
|
||||
modules: {
|
||||
transport: [
|
||||
WS,
|
||||
WebRTCStar
|
||||
],
|
||||
streamMuxer: [MPLEX],
|
||||
connEncryption: [SECIO],
|
||||
},
|
||||
config: {
|
||||
peerDiscovery: {
|
||||
webRTCStar: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### Customizing Pubsub
|
||||
|
||||
```js
|
||||
const Libp2p = require('libp2p')
|
||||
const TCP = require('libp2p-tcp')
|
||||
const MPLEX = require('libp2p-mplex')
|
||||
const SECIO = require('libp2p-secio')
|
||||
const GossipSub = require('libp2p-gossipsub')
|
||||
|
||||
const node = await Libp2p.create({
|
||||
modules: {
|
||||
transport: [TCP],
|
||||
streamMuxer: [MPLEX],
|
||||
connEncryption: [SECIO],
|
||||
pubsub: GossipSub
|
||||
},
|
||||
config: {
|
||||
pubsub: { // The pubsub options (and defaults) can be found in the pubsub router documentation
|
||||
enabled: true,
|
||||
emitSelf: true, // whether the node should emit to self on publish
|
||||
signMessages: true, // if messages should be signed
|
||||
strictSigning: true // if message signing should be required
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### Customizing DHT
|
||||
|
||||
```js
|
||||
const Libp2p = require('libp2p')
|
||||
const TCP = require('libp2p-tcp')
|
||||
const MPLEX = require('libp2p-mplex')
|
||||
const SECIO = require('libp2p-secio')
|
||||
const DHT = require('libp2p-kad-dht')
|
||||
|
||||
const node = await Libp2p.create({
|
||||
modules: {
|
||||
transport: [TCP],
|
||||
streamMuxer: [MPLEX],
|
||||
connEncryption: [SECIO],
|
||||
dht: DHT
|
||||
},
|
||||
config: {
|
||||
dht: { // The DHT options (and defaults) can be found in its documentation
|
||||
kBucketSize: 20,
|
||||
enabled: true,
|
||||
randomWalk: {
|
||||
enabled: true, // Allows to disable discovery (enabled by default)
|
||||
interval: 300e3,
|
||||
timeout: 10e3
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### Setup with Content and Peer Routing
|
||||
|
||||
```js
|
||||
const Libp2p = require('libp2p')
|
||||
const TCP = require('libp2p-tcp')
|
||||
const MPLEX = require('libp2p-mplex')
|
||||
const SECIO = require('libp2p-secio')
|
||||
const DelegatedPeerRouter = require('libp2p-delegated-peer-routing')
|
||||
const DelegatedContentRouter = require('libp2p-delegated-content-routing')
|
||||
const PeerInfo = require('peer-info')
|
||||
|
||||
// create a peerInfo
|
||||
const peerInfo = await PeerInfo.create()
|
||||
|
||||
const node = await Libp2p.create({
|
||||
modules: {
|
||||
transport: [TCP],
|
||||
streamMuxer: [MPLEX],
|
||||
connEncryption: [SECIO],
|
||||
contentRouting: [
|
||||
new DelegatedContentRouter(peerInfo.id)
|
||||
],
|
||||
peerRouting: [
|
||||
new DelegatedPeerRouter()
|
||||
],
|
||||
},
|
||||
peerInfo
|
||||
})
|
||||
```
|
||||
|
||||
#### Setup with Relay
|
||||
|
||||
```js
|
||||
const Libp2p = require('libp2p')
|
||||
const TCP = require('libp2p-tcp')
|
||||
const MPLEX = require('libp2p-mplex')
|
||||
const SECIO = require('libp2p-secio')
|
||||
|
||||
const node = await Libp2p.create({
|
||||
modules: {
|
||||
transport: [TCP],
|
||||
streamMuxer: [MPLEX],
|
||||
connEncryption: [SECIO]
|
||||
},
|
||||
config: {
|
||||
relay: { // Circuit Relay options (this config is part of libp2p core configurations)
|
||||
enabled: true, // Allows you to dial and accept relayed connections. Does not make you a relay.
|
||||
hop: {
|
||||
enabled: true, // Allows you to be a relay for other peers
|
||||
active: true // You will attempt to dial destination peers if you are not connected to them
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### Configuring Connection Manager
|
||||
|
||||
The Connection Manager prunes Connections in libp2p whenever certain limits are exceeded. If Metrics are enabled, you can also configure the Connection Manager to monitor the bandwidth of libp2p and prune connections as needed. You can read more about what Connection Manager does at [./CONNECTION_MANAGER.md](./CONNECTION_MANAGER.md). The configuration values below show the defaults for Connection Manager. See [./CONNECTION_MANAGER.md](./CONNECTION_MANAGER.md#options) for a full description of the parameters.
|
||||
|
||||
```js
|
||||
const Libp2p = require('libp2p')
|
||||
const TCP = require('libp2p-tcp')
|
||||
const MPLEX = require('libp2p-mplex')
|
||||
const SECIO = require('libp2p-secio')
|
||||
|
||||
const node = await Libp2p.create({
|
||||
modules: {
|
||||
transport: [TCP],
|
||||
streamMuxer: [MPLEX],
|
||||
connEncryption: [SECIO]
|
||||
},
|
||||
connectionManager: {
|
||||
maxConnections: Infinity,
|
||||
minConnections: 0,
|
||||
pollInterval: 2000,
|
||||
defaultPeerValue: 1,
|
||||
// The below values will only be taken into account when Metrics are enabled
|
||||
maxData: Infinity,
|
||||
maxSentData: Infinity,
|
||||
maxReceivedData: Infinity,
|
||||
maxEventLoopDelay: Infinity,
|
||||
movingAverageInterval: 60000
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### Configuring Metrics
|
||||
|
||||
Metrics are disabled in libp2p by default. You can enable and configure them as follows. Aside from enabled being `false` by default, the configuration options listed here are the current defaults.
|
||||
|
||||
```js
|
||||
const Libp2p = require('libp2p')
|
||||
const TCP = require('libp2p-tcp')
|
||||
const MPLEX = require('libp2p-mplex')
|
||||
const SECIO = require('libp2p-secio')
|
||||
|
||||
const node = await Libp2p.create({
|
||||
modules: {
|
||||
transport: [TCP],
|
||||
streamMuxer: [MPLEX],
|
||||
connEncryption: [SECIO]
|
||||
},
|
||||
metrics: {
|
||||
enabled: true,
|
||||
computeThrottleMaxQueueSize: 1000, // How many messages a stat will queue before processing
|
||||
computeThrottleTimeout: 2000, // Time in milliseconds a stat will wait, after the last item was added, before processing
|
||||
movingAverageIntervals: [ // The moving averages that will be computed
|
||||
60 * 1000, // 1 minute
|
||||
5 * 60 * 1000, // 5 minutes
|
||||
15 * 60 * 1000 // 15 minutes
|
||||
],
|
||||
maxOldPeersRetention: 50 // How many disconnected peers we will retain stats for
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Configuration examples
|
||||
|
||||
As libp2p is designed to be a modular networking library, its usage will vary based on individual project needs. We've included links to some existing project configurations for your reference, in case you wish to replicate their configuration:
|
||||
|
||||
|
||||
- [libp2p-ipfs-nodejs](https://github.com/ipfs/js-ipfs/tree/master/src/core/runtime/libp2p-nodejs.js) - libp2p configuration used by js-ipfs when running in Node.js
|
||||
- [libp2p-ipfs-browser](https://github.com/ipfs/js-ipfs/tree/master/src/core/runtime/libp2p-browser.js) - libp2p configuration used by js-ipfs when running in a Browser (that supports WebRTC)
|
||||
|
||||
If you have developed a project using `js-libp2p`, please consider submitting your configuration to this list so that it can be found easily by other users.
|
||||
|
||||
The [examples](../examples) are also a good source of help for finding a configuration that suits your needs.
|
20
doc/CONNECTION_MANAGER.md
Normal file
20
doc/CONNECTION_MANAGER.md
Normal file
@ -0,0 +1,20 @@
|
||||
# Connection Manager
|
||||
|
||||
The Connection Manager works with the Registrar to keep connections across libp2p within acceptable ranges, which can be configured. By default Connection Manager will monitor:
|
||||
- The total number of open connections
|
||||
- The latency/delay of the event loop
|
||||
|
||||
If Metrics are enabled for libp2p, see [./CONFIGURATION.md#configuring-metrics](./CONFIGURATION.md#configuring-metrics) on how to configure metrics, the Connection Manager can be used to prune connections when certain limits are exceeded.
|
||||
|
||||
The following is a list of available options for setting limits for the Connection Manager to enforce.
|
||||
|
||||
## Options
|
||||
- `maxConnections`: the maximum number of connections libp2p is willing to have before it starts disconnecting. Defaults to `Infinity`
|
||||
- `minConnections`: the minimum number of connections below which libp2p not activate preemptive disconnections. Defaults to `0`.
|
||||
- `maxData`: sets the maximum data — in bytes per second - (sent and received) this node is willing to endure before it starts disconnecting peers. Defaults to `Infinity`.
|
||||
- `maxSentData`: sets the maximum sent data — in bytes per second - this node is willing to endure before it starts disconnecting peers. Defaults to `Infinity`.
|
||||
- `maxReceivedData`: sets the maximum received data — in bytes per second - this node is willing to endure before it starts disconnecting peers. Defaults to `Infinity`.
|
||||
- `maxEventLoopDelay`: sets the maximum event loop delay (measured in milliseconds) this node is willing to endure before it starts disconnecting peers. Defaults to `Infinity`.
|
||||
- `pollInterval`: sets the poll interval (in milliseconds) for assessing the current state and determining if this peer needs to force a disconnect. Defaults to `2000` (2 seconds).
|
||||
- `movingAverageInterval`: the interval used to calculate moving averages (in milliseconds). Defaults to `60000` (1 minute). This must be an available interval configured in `Metrics`
|
||||
- `defaultPeerValue`: number between 0 and 1. Defaults to 1.
|
32
doc/DIALER.md
Normal file
32
doc/DIALER.md
Normal file
@ -0,0 +1,32 @@
|
||||
# js-libp2p Dialer
|
||||
|
||||
**Synopsis**
|
||||
* Parallel dials to the same peer will yield the same connection/error when the first dial settles.
|
||||
* All Dial Requests in js-libp2p must request a token(s) from the Dialer.
|
||||
* The number of tokens requested should be between 1 and the MAX_PER_PEER_DIALS max set in the Dialer.
|
||||
* If the number of available tokens is less than requested, the Dialer may return less than requested.
|
||||
* The number of tokens a DialRequest obtains reflects the maximum number of parallel Multiaddr Dials it can make.
|
||||
* If no tokens are available a DialRequest should immediately end and throw.
|
||||
* As tokens are limited, DialRequests should be given a prioritized list of Multiaddrs to minimize the potential request time.
|
||||
* Once a Multiaddr Dial has succeeded, all pending dials in that Dial Request should be aborted.
|
||||
* If DIAL_TIMEOUT time has elapsed before any one Multiaddr Dial succeeds, all remaining dials in the DialRequest should be aborted.
|
||||
* When a Multiaddr Dial is settled, if there are no more addresses to dial, its token should be released back to the dialer.
|
||||
* Once the DialRequest is settled, any remaining tokens should be released to the dialer.
|
||||
|
||||
## Multiaddr Confidence
|
||||
|
||||
An effective dialing system should involve the inclusion of a Confidence system for Multiaddrs. This enables ranking of Multiaddrs so that a prioritized list can be passed to DialRequests to maximize usage of Dialer Tokens, and minimize connection times.
|
||||
|
||||
**Not Yet Implemented**: This system will be designed and implemented in a future update.
|
||||
|
||||
## Notes
|
||||
|
||||
* A DialRequest gets a set of tokens from the Dialer, up to the MAX_PER_PEER_DIALS max.
|
||||
* A DialRequest SHOULD fail if no dial tokens are available. The DialRequest MAY proceed without tokens, but this should be reserved for High Priority actions and should be rare.
|
||||
* A DialRequest MUST NOT request more tokens than it has addresses to dial. Example: If the MAX_PER_PEER_DIALS is 4 and a DialRequest has 1 address, it should only request 1 token.
|
||||
* A DialRequest SHOULD execute parallel dials for each of its addresses up the total number of tokens it has.
|
||||
* On a successful dial, the DialRequest MUST abort any other in progress dials, return the successful connection and release all tokens.
|
||||
* A new DialRequest SHOULD be given a descending list of prioritized Multiaddrs, based on their confidence. Having higher confidence Multiaddrs first can help minimize the time a DialRequest is active.
|
||||
* A failed dial to a Multiaddr SHOULD add that Multiaddr to a temporary denyList.
|
||||
* A failed dial to a Multiaddr SHOULD lower the confidence of that Multiaddr.
|
||||
* A successful dial to a Multiaddr SHOULD increase the confidence of that Multiaddr.
|
28
doc/METRICS.md
Normal file
28
doc/METRICS.md
Normal file
@ -0,0 +1,28 @@
|
||||
# Bandwidth Metrics
|
||||
|
||||
- Metrics gathering is optional, as there is a performance hit to using it.
|
||||
- Metrics do NOT currently contain OS level stats, only libp2p application level Metrics. For example, TCP messages (ACK, FIN, etc) are not accounted for.
|
||||
- See the [API](./API.md) for Metrics usage. Metrics in libp2p do not emit events, as such applications wishing to read Metrics will need to do so actively. This ensures that the system is not unnecessarily firing update notifications.
|
||||
|
||||
## Tracking
|
||||
- When a transport hands off a connection for upgrading, Metrics are hooked up if enabled.
|
||||
- When a stream is created, Metrics will be tracked on that stream and associated to that streams protocol.
|
||||
- Tracked Metrics are associated to a specific peer, and count towards global bandwidth Metrics.
|
||||
|
||||
### Metrics Processing
|
||||
- The main Metrics object consists of individual `Stats` objects
|
||||
- The following categories are tracked:
|
||||
- Global stats; every byte in and out
|
||||
- Peer stats; every byte in and out, per peer
|
||||
- Protocol stats; every byte in and out, per protocol
|
||||
- When a message goes through Metrics:
|
||||
- It is added to the global stat
|
||||
- It is added to the stats for the remote peer
|
||||
- It is added to the protocol stats if there is one
|
||||
- When data is pushed onto a `Stat` it is added to a queue
|
||||
- The queue is processed at the earliest of either (configurable):
|
||||
- every 2 seconds after the last item was added to the queue
|
||||
- or once 1000 items have been queued
|
||||
- When the queue is processed:
|
||||
- The data length is added to either the `in` or `out` stat
|
||||
- The moving averages is calculated since the last queue processing (based on most recently processed item timestamp)
|
164
doc/STREAMING_ITERABLES.md
Normal file
164
doc/STREAMING_ITERABLES.md
Normal file
@ -0,0 +1,164 @@
|
||||
# Iterable Streams
|
||||
|
||||
> This document is a guide on how to use Iterable Streams in Libp2p. As a part of the [refactor away from callbacks](https://github.com/ipfs/js-ipfs/issues/1670), we have also moved to using Iterable Streams instead of [pull-streams](https://pull-stream.github.io/). If there are missing usage guides you feel should be added, please submit a PR!
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Iterable Streams](#iterable-streams)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Usage Guide](#usage-guide)
|
||||
- [Transforming Bidirectional Data](#transforming-bidirectional-data)
|
||||
- [Iterable Stream Types](#iterable-stream-types)
|
||||
- [Source](#source)
|
||||
- [Sink](#sink)
|
||||
- [Transform](#transform)
|
||||
- [Duplex](#duplex)
|
||||
- [Iterable Modules](#iterable-modules)
|
||||
|
||||
## Usage Guide
|
||||
|
||||
### Transforming Bidirectional Data
|
||||
|
||||
Sometimes you may need to wrap an existing duplex stream in order to perform incoming and outgoing [transforms](#transform) on data. This type of wrapping is commonly used in stream encryption/decryption. Using [it-pair][it-pair] and [it-pipe][it-pipe], we can do this rather easily, given an existing [duplex iterable](#duplex).
|
||||
|
||||
```js
|
||||
const duplexPair = require('it-pair/duplex')
|
||||
const pipe = require('it-pipe')
|
||||
|
||||
// Wrapper is what we will write and read from
|
||||
// This gives us two duplex iterables that are internally connected
|
||||
const [internal, external] = duplexPair()
|
||||
|
||||
// Now we can pipe our wrapper to the existing duplex iterable
|
||||
pipe(
|
||||
external, // The external half of the pair interacts with the existing duplex
|
||||
outgoingTransform, // A transform iterable to send data through (ie: encrypting)
|
||||
existingDuplex, // The original duplex iterable we are wrapping
|
||||
incomingTransform, // A transform iterable to read data through (ie: decrypting)
|
||||
external
|
||||
)
|
||||
|
||||
// We can now read and write from the other half of our pair
|
||||
pipe(
|
||||
['some data'],
|
||||
internal, // The internal half of the pair is what we will interact with to read/write data
|
||||
async (source) => {
|
||||
for await (const chunk of source) {
|
||||
console.log('Data: %s', chunk.toString())
|
||||
// > Data: some data
|
||||
}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Iterable Stream Types
|
||||
|
||||
These types are pulled from [@alanshaw's gist](https://gist.github.com/alanshaw/591dc7dd54e4f99338a347ef568d6ee9) on streaming iterables.
|
||||
|
||||
### Source
|
||||
|
||||
A "source" is something that can be consumed. It is an iterable object.
|
||||
|
||||
```js
|
||||
const ints = {
|
||||
[Symbol.asyncIterator] () {
|
||||
let i = 0
|
||||
return {
|
||||
async next () {
|
||||
return { done: false, value: i++ }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// or, more succinctly using a generator and for/await:
|
||||
|
||||
const ints = (async function * () {
|
||||
let i = 0
|
||||
while (true) yield i++
|
||||
})()
|
||||
```
|
||||
|
||||
### Sink
|
||||
|
||||
A "sink" is something that consumes (or drains) a source. It is a function that takes a source and iterates over it. It optionally returns a value.
|
||||
|
||||
```js
|
||||
const logger = async source => {
|
||||
const it = source[Symbol.asyncIterator]()
|
||||
while (true) {
|
||||
const { done, value } = await it.next()
|
||||
if (done) break
|
||||
console.log(value) // prints 0, 1, 2, 3...
|
||||
}
|
||||
}
|
||||
|
||||
// or, more succinctly using a generator and for/await:
|
||||
|
||||
const logger = async source => {
|
||||
for await (const chunk of source) {
|
||||
console.log(chunk) // prints 0, 1, 2, 3...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Transform
|
||||
|
||||
A "transform" is both a sink _and_ a source where the values it consumes and the values that can be consumed from it are connected in some way. It is a function that takes a source and returns a source.
|
||||
|
||||
```js
|
||||
const doubler = source => {
|
||||
return {
|
||||
[Symbol.asyncIterator] () {
|
||||
const it = source[Symbol.asyncIterator]()
|
||||
return {
|
||||
async next () {
|
||||
const { done, value } = await it.next()
|
||||
if (done) return { done }
|
||||
return { done, value: value * 2 }
|
||||
}
|
||||
return () {
|
||||
return it.return && it.return()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// or, more succinctly using a generator and for/await:
|
||||
|
||||
const doubler = source => (async function * () {
|
||||
for await (const chunk of source) {
|
||||
yield chunk * 2
|
||||
}
|
||||
})()
|
||||
```
|
||||
|
||||
### Duplex
|
||||
|
||||
A "duplex" is similar to a transform but the values it consumes are not necessarily connected to the values that can be consumed from it. It is an object with two properties, `sink` and `source`.
|
||||
|
||||
```js
|
||||
const duplex = {
|
||||
sink: async source => {/* ... */},
|
||||
source: { [Symbol.asyncIterator] () {/* ... */} }
|
||||
}
|
||||
```
|
||||
|
||||
## Iterable Modules
|
||||
|
||||
- [it-handshake][it-handshake] Handshakes for binary protocols with iterable streams.
|
||||
- [it-length-prefixed][it-length-prefixed] Streaming length prefixed buffers with async iterables.
|
||||
- [it-pair][it-pair] Paired streams that are internally connected.
|
||||
- [it-pipe][it-pipe] Create a pipeline of iterables. Works with duplex streams.
|
||||
- [it-pushable][it-pushable] An iterable that you can push values into.
|
||||
- [it-reader][it-reader] Read an exact number of bytes from a binary, async iterable.
|
||||
- [streaming-iterables][streaming-iterables] A Swiss army knife for async iterables.
|
||||
|
||||
[it-handshake]: https://github.com/jacobheun/it-handshake
|
||||
[it-length-prefixed]: https://github.com/alanshaw/it-length-prefixed
|
||||
[it-pair]: https://github.com/alanshaw/it-pair
|
||||
[it-pipe]: https://github.com/alanshaw/it-pipe
|
||||
[it-pushable]: https://github.com/alanshaw/it-pushable
|
||||
[it-reader]: https://github.com/alanshaw/it-reader
|
||||
[streaming-iterables]: https://github.com/bustle/streaming-iterables
|
@ -1,8 +1,8 @@
|
||||
# `js-libp2p` Examples and Tutorials
|
||||
|
||||
In this folder, you can find a variety of examples to help you get started in using js-libp2p, in Node.js and in the Browser. Every example as a specific purpose and some of each incorporate a full tutorial that you can follow through, helping you expand your knowledge about libp2p and p2p networks in general.
|
||||
In this folder, you can find a variety of examples to help you get started in using js-libp2p, in Node.js and in the Browser. Every example has a specific purpose and some incorporate a full tutorial that you can follow through, helping you expand your knowledge about libp2p and p2p networks in general.
|
||||
|
||||
Let us know if you find any issue or if you want to contribute and add a new tutorial, feel welcome to submit a PR, thank you!
|
||||
Let us know if you find any issues, or if you want to contribute and add a new tutorial, feel free to submit a PR, thank you!
|
||||
|
||||
## Understanding how libp2p works
|
||||
|
||||
@ -22,3 +22,5 @@ Let us know if you find any issue or if you want to contribute and add a new tut
|
||||
- Running libp2p in the Electron (future)
|
||||
- [The standard echo net example with libp2p](./echo)
|
||||
- [A simple chat app with libp2p](./chat)
|
||||
|
||||
For go-libp2p examples, check out https://github.com/libp2p/go-libp2p-examples#examples-and-tutorials
|
||||
|
@ -4,76 +4,44 @@
|
||||
const PeerId = require('peer-id')
|
||||
const PeerInfo = require('peer-info')
|
||||
const Node = require('./libp2p-bundle')
|
||||
const pull = require('pull-stream')
|
||||
const async = require('async')
|
||||
const Pushable = require('pull-pushable')
|
||||
const p = Pushable()
|
||||
let idListener
|
||||
const { stdinToStream, streamToConsole } = require('./stream')
|
||||
|
||||
async.parallel([
|
||||
(callback) => {
|
||||
PeerId.createFromJSON(require('./peer-id-dialer'), (err, idDialer) => {
|
||||
if (err) {
|
||||
throw err
|
||||
}
|
||||
callback(null, idDialer)
|
||||
})
|
||||
},
|
||||
(callback) => {
|
||||
PeerId.createFromJSON(require('./peer-id-listener'), (err, idListener) => {
|
||||
if (err) {
|
||||
throw err
|
||||
}
|
||||
callback(null, idListener)
|
||||
})
|
||||
}
|
||||
], (err, ids) => {
|
||||
if (err) throw err
|
||||
const peerDialer = new PeerInfo(ids[0])
|
||||
async function run() {
|
||||
const [idDialer, idListener] = await Promise.all([
|
||||
PeerId.createFromJSON(require('./peer-id-dialer')),
|
||||
PeerId.createFromJSON(require('./peer-id-listener'))
|
||||
])
|
||||
|
||||
// Create a new libp2p node on localhost with a randomly chosen port
|
||||
const peerDialer = new PeerInfo(idDialer)
|
||||
peerDialer.multiaddrs.add('/ip4/0.0.0.0/tcp/0')
|
||||
const nodeDialer = new Node({
|
||||
peerInfo: peerDialer
|
||||
})
|
||||
|
||||
const peerListener = new PeerInfo(ids[1])
|
||||
idListener = ids[1]
|
||||
// Create a PeerInfo with the listening peer's address
|
||||
const peerListener = new PeerInfo(idListener)
|
||||
peerListener.multiaddrs.add('/ip4/127.0.0.1/tcp/10333')
|
||||
nodeDialer.start((err) => {
|
||||
if (err) {
|
||||
throw err
|
||||
}
|
||||
|
||||
console.log('Dialer ready, listening on:')
|
||||
// Start the libp2p host
|
||||
await nodeDialer.start()
|
||||
|
||||
peerListener.multiaddrs.forEach((ma) => {
|
||||
console.log(ma.toString() + '/p2p/' + idListener.toB58String())
|
||||
})
|
||||
|
||||
nodeDialer.dialProtocol(peerListener, '/chat/1.0.0', (err, conn) => {
|
||||
if (err) {
|
||||
throw err
|
||||
}
|
||||
console.log('nodeA dialed to nodeB on protocol: /chat/1.0.0')
|
||||
console.log('Type a message and see what happens')
|
||||
// Write operation. Data sent as a buffer
|
||||
pull(
|
||||
p,
|
||||
conn
|
||||
)
|
||||
// Sink, data converted from buffer to utf8 string
|
||||
pull(
|
||||
conn,
|
||||
pull.map((data) => {
|
||||
return data.toString('utf8').replace('\n', '')
|
||||
}),
|
||||
pull.drain(console.log)
|
||||
)
|
||||
|
||||
process.stdin.setEncoding('utf8')
|
||||
process.openStdin().on('data', (chunk) => {
|
||||
var data = chunk.toString()
|
||||
p.push(data)
|
||||
})
|
||||
})
|
||||
// Output this node's address
|
||||
console.log('Dialer ready, listening on:')
|
||||
peerListener.multiaddrs.forEach((ma) => {
|
||||
console.log(ma.toString() + '/p2p/' + idListener.toB58String())
|
||||
})
|
||||
})
|
||||
|
||||
// Dial to the remote peer (the "listener")
|
||||
const { stream } = await nodeDialer.dialProtocol(peerListener, '/chat/1.0.0')
|
||||
|
||||
console.log('Dialer dialed to listener on protocol: /chat/1.0.0')
|
||||
console.log('Type a message and see what happens')
|
||||
|
||||
// Send stdin to the stream
|
||||
stdinToStream(stream)
|
||||
// Read the stream and output to console
|
||||
streamToConsole(stream)
|
||||
}
|
||||
|
||||
run()
|
||||
|
@ -1,41 +1,12 @@
|
||||
'use strict'
|
||||
|
||||
const TCP = require('libp2p-tcp')
|
||||
const MulticastDNS = require('libp2p-mdns')
|
||||
const WS = require('libp2p-websockets')
|
||||
const Bootstrap = require('libp2p-bootstrap')
|
||||
const spdy = require('libp2p-spdy')
|
||||
const KadDHT = require('libp2p-kad-dht')
|
||||
const mplex = require('libp2p-mplex')
|
||||
const secio = require('libp2p-secio')
|
||||
const defaultsDeep = require('@nodeutils/defaults-deep')
|
||||
const libp2p = require('../../..')
|
||||
|
||||
function mapMuxers (list) {
|
||||
return list.map((pref) => {
|
||||
if (typeof pref !== 'string') {
|
||||
return pref
|
||||
}
|
||||
switch (pref.trim().toLowerCase()) {
|
||||
case 'spdy': return spdy
|
||||
case 'mplex': return mplex
|
||||
default:
|
||||
throw new Error(pref + ' muxer not available')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getMuxers (muxers) {
|
||||
const muxerPrefs = process.env.LIBP2P_MUXER
|
||||
if (muxerPrefs && !muxers) {
|
||||
return mapMuxers(muxerPrefs.split(','))
|
||||
} else if (muxers) {
|
||||
return mapMuxers(muxers)
|
||||
} else {
|
||||
return [mplex, spdy]
|
||||
}
|
||||
}
|
||||
|
||||
class Node extends libp2p {
|
||||
constructor (_options) {
|
||||
const defaults = {
|
||||
@ -44,29 +15,8 @@ class Node extends libp2p {
|
||||
TCP,
|
||||
WS
|
||||
],
|
||||
streamMuxer: getMuxers(_options.muxer),
|
||||
connEncryption: [ secio ],
|
||||
peerDiscovery: [
|
||||
MulticastDNS,
|
||||
Bootstrap
|
||||
],
|
||||
dht: KadDHT
|
||||
},
|
||||
config: {
|
||||
peerDiscovery: {
|
||||
mdns: {
|
||||
interval: 10000,
|
||||
enabled: false
|
||||
},
|
||||
bootstrap: {
|
||||
interval: 10000,
|
||||
enabled: false,
|
||||
list: _options.bootstrapList
|
||||
}
|
||||
},
|
||||
dht: {
|
||||
kBucketSize: 20
|
||||
}
|
||||
streamMuxer: [ mplex ],
|
||||
connEncryption: [ secio ]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,53 +4,38 @@
|
||||
const PeerId = require('peer-id')
|
||||
const PeerInfo = require('peer-info')
|
||||
const Node = require('./libp2p-bundle.js')
|
||||
const pull = require('pull-stream')
|
||||
const Pushable = require('pull-pushable')
|
||||
const p = Pushable()
|
||||
const { stdinToStream, streamToConsole } = require('./stream')
|
||||
|
||||
PeerId.createFromJSON(require('./peer-id-listener'), (err, idListener) => {
|
||||
if (err) {
|
||||
throw err
|
||||
}
|
||||
async function run() {
|
||||
// Create a new libp2p node with the given multi-address
|
||||
const idListener = await PeerId.createFromJSON(require('./peer-id-listener'))
|
||||
const peerListener = new PeerInfo(idListener)
|
||||
peerListener.multiaddrs.add('/ip4/0.0.0.0/tcp/10333')
|
||||
const nodeListener = new Node({
|
||||
peerInfo: peerListener
|
||||
})
|
||||
|
||||
nodeListener.start((err) => {
|
||||
if (err) {
|
||||
throw err
|
||||
}
|
||||
|
||||
nodeListener.on('peer:connect', (peerInfo) => {
|
||||
console.log(peerInfo.id.toB58String())
|
||||
})
|
||||
|
||||
nodeListener.handle('/chat/1.0.0', (protocol, conn) => {
|
||||
pull(
|
||||
p,
|
||||
conn
|
||||
)
|
||||
|
||||
pull(
|
||||
conn,
|
||||
pull.map((data) => {
|
||||
return data.toString('utf8').replace('\n', '')
|
||||
}),
|
||||
pull.drain(console.log)
|
||||
)
|
||||
|
||||
process.stdin.setEncoding('utf8')
|
||||
process.openStdin().on('data', (chunk) => {
|
||||
var data = chunk.toString()
|
||||
p.push(data)
|
||||
})
|
||||
})
|
||||
|
||||
console.log('Listener ready, listening on:')
|
||||
peerListener.multiaddrs.forEach((ma) => {
|
||||
console.log(ma.toString() + '/p2p/' + idListener.toB58String())
|
||||
})
|
||||
// Log a message when a remote peer connects to us
|
||||
nodeListener.on('peer:connect', (peerInfo) => {
|
||||
console.log(peerInfo.id.toB58String())
|
||||
})
|
||||
})
|
||||
|
||||
// Handle messages for the protocol
|
||||
await nodeListener.handle('/chat/1.0.0', async ({ stream }) => {
|
||||
// Send stdin to the stream
|
||||
stdinToStream(stream)
|
||||
// Read the stream and output to console
|
||||
streamToConsole(stream)
|
||||
})
|
||||
|
||||
// Start listening
|
||||
await nodeListener.start()
|
||||
|
||||
// Output listen addresses to the console
|
||||
console.log('Listener ready, listening on:')
|
||||
peerListener.multiaddrs.forEach((ma) => {
|
||||
console.log(ma.toString() + '/p2p/' + idListener.toB58String())
|
||||
})
|
||||
}
|
||||
|
||||
run()
|
||||
|
40
examples/chat/src/stream.js
Normal file
40
examples/chat/src/stream.js
Normal file
@ -0,0 +1,40 @@
|
||||
'use strict'
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const pipe = require('it-pipe')
|
||||
const lp = require('it-length-prefixed')
|
||||
|
||||
function stdinToStream(stream) {
|
||||
// Read utf-8 from stdin
|
||||
process.stdin.setEncoding('utf8')
|
||||
pipe(
|
||||
// Read from stdin (the source)
|
||||
process.stdin,
|
||||
// Encode with length prefix (so receiving side knows how much data is coming)
|
||||
lp.encode(),
|
||||
// Write to the stream (the sink)
|
||||
stream.sink
|
||||
)
|
||||
}
|
||||
|
||||
function streamToConsole(stream) {
|
||||
pipe(
|
||||
// Read from the stream (the source)
|
||||
stream.source,
|
||||
// Decode length-prefixed data
|
||||
lp.decode(),
|
||||
// Sink function
|
||||
async function (source) {
|
||||
// For each chunk of data
|
||||
for await (const msg of source) {
|
||||
// Output the data as a utf8 string
|
||||
console.log('> ' + msg.toString('utf8').replace('\n', ''))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
stdinToStream,
|
||||
streamToConsole
|
||||
}
|
@ -44,7 +44,7 @@ class App extends Component {
|
||||
isLoading: this.state.isLoading + 1
|
||||
})
|
||||
|
||||
this.ipfs.files.cat(this.state.hash, (err, data) => {
|
||||
this.ipfs.cat(this.state.hash, (err, data) => {
|
||||
if (err) console.log('Error', err)
|
||||
|
||||
this.setState({
|
||||
|
@ -8,51 +8,54 @@
|
||||
const PeerId = require('peer-id')
|
||||
const PeerInfo = require('peer-info')
|
||||
const Node = require('./libp2p-bundle')
|
||||
const pull = require('pull-stream')
|
||||
const async = require('async')
|
||||
const pipe = require('it-pipe')
|
||||
|
||||
async.parallel([
|
||||
(cb) => PeerId.createFromJSON(require('./id-d'), cb),
|
||||
(cb) => PeerId.createFromJSON(require('./id-l'), cb)
|
||||
], (err, ids) => {
|
||||
if (err) { throw err }
|
||||
async function run() {
|
||||
const [dialerId, listenerId] = await Promise.all([
|
||||
PeerId.createFromJSON(require('./id-d')),
|
||||
PeerId.createFromJSON(require('./id-l'))
|
||||
])
|
||||
|
||||
// Dialer
|
||||
const dialerId = ids[0]
|
||||
const dialerPeerInfo = new PeerInfo(dialerId)
|
||||
dialerPeerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0')
|
||||
const dialerNode = new Node({
|
||||
peerInfo: dialerPeerInfo
|
||||
})
|
||||
|
||||
// Peer to Dial
|
||||
const listenerPeerInfo = new PeerInfo(ids[1])
|
||||
const listenerId = ids[1]
|
||||
// Peer to Dial (the listener)
|
||||
const listenerPeerInfo = new PeerInfo(listenerId)
|
||||
const listenerMultiaddr = '/ip4/127.0.0.1/tcp/10333/p2p/' +
|
||||
listenerId.toB58String()
|
||||
listenerPeerInfo.multiaddrs.add(listenerMultiaddr)
|
||||
|
||||
dialerNode.start((err) => {
|
||||
if (err) { throw err }
|
||||
// Start the dialer libp2p node
|
||||
await dialerNode.start()
|
||||
|
||||
console.log('Dialer ready, listening on:')
|
||||
dialerPeerInfo.multiaddrs.forEach((ma) => console.log(ma.toString() +
|
||||
'/p2p/' + dialerId.toB58String()))
|
||||
console.log('Dialer ready, listening on:')
|
||||
dialerPeerInfo.multiaddrs.forEach((ma) => console.log(ma.toString() +
|
||||
'/p2p/' + dialerId.toB58String()))
|
||||
|
||||
console.log('Dialing to peer:', listenerMultiaddr.toString())
|
||||
dialerNode.dialProtocol(listenerPeerInfo, '/echo/1.0.0', (err, conn) => {
|
||||
if (err) { throw err }
|
||||
// Dial the listener node
|
||||
console.log('Dialing to peer:', listenerMultiaddr.toString())
|
||||
const { stream } = await dialerNode.dialProtocol(listenerPeerInfo, '/echo/1.0.0')
|
||||
|
||||
console.log('nodeA dialed to nodeB on protocol: /echo/1.0.0')
|
||||
console.log('nodeA dialed to nodeB on protocol: /echo/1.0.0')
|
||||
|
||||
pull(
|
||||
pull.values(['hey']),
|
||||
conn,
|
||||
pull.collect((err, data) => {
|
||||
if (err) { throw err }
|
||||
console.log('received echo:', data.toString())
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
pipe(
|
||||
// Source data
|
||||
['hey'],
|
||||
// Write to the stream, and pass its output to the next function
|
||||
stream,
|
||||
// Sink function
|
||||
async function (source) {
|
||||
// For each chunk of data
|
||||
for await (const data of source) {
|
||||
// Output the data
|
||||
console.log('received echo:', data.toString())
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
run()
|
||||
|
@ -1,41 +1,12 @@
|
||||
'use strict'
|
||||
|
||||
const TCP = require('libp2p-tcp')
|
||||
const MulticastDNS = require('libp2p-mdns')
|
||||
const WS = require('libp2p-websockets')
|
||||
const Bootstrap = require('libp2p-bootstrap')
|
||||
const spdy = require('libp2p-spdy')
|
||||
const KadDHT = require('libp2p-kad-dht')
|
||||
const mplex = require('libp2p-mplex')
|
||||
const secio = require('libp2p-secio')
|
||||
const defaultsDeep = require('@nodeutils/defaults-deep')
|
||||
const libp2p = require('../../..')
|
||||
|
||||
function mapMuxers (list) {
|
||||
return list.map((pref) => {
|
||||
if (typeof pref !== 'string') {
|
||||
return pref
|
||||
}
|
||||
switch (pref.trim().toLowerCase()) {
|
||||
case 'spdy': return spdy
|
||||
case 'mplex': return mplex
|
||||
default:
|
||||
throw new Error(pref + ' muxer not available')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getMuxers (muxers) {
|
||||
const muxerPrefs = process.env.LIBP2P_MUXER
|
||||
if (muxerPrefs && !muxers) {
|
||||
return mapMuxers(muxerPrefs.split(','))
|
||||
} else if (muxers) {
|
||||
return mapMuxers(muxers)
|
||||
} else {
|
||||
return [mplex, spdy]
|
||||
}
|
||||
}
|
||||
|
||||
class Node extends libp2p {
|
||||
constructor (_options) {
|
||||
const defaults = {
|
||||
@ -44,29 +15,8 @@ class Node extends libp2p {
|
||||
TCP,
|
||||
WS
|
||||
],
|
||||
streamMuxer: getMuxers(_options.muxer),
|
||||
connEncryption: [ secio ],
|
||||
peerDiscovery: [
|
||||
MulticastDNS,
|
||||
Bootstrap
|
||||
],
|
||||
dht: KadDHT
|
||||
},
|
||||
config: {
|
||||
peerDiscovery: {
|
||||
mdns: {
|
||||
interval: 10000,
|
||||
enabled: false
|
||||
},
|
||||
bootstrap: {
|
||||
interval: 10000,
|
||||
enabled: false,
|
||||
list: _options.bootstrapList
|
||||
}
|
||||
},
|
||||
dht: {
|
||||
kBucketSize: 20
|
||||
}
|
||||
streamMuxer: [ mplex ],
|
||||
connEncryption: [ secio ]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,39 +8,34 @@
|
||||
const PeerId = require('peer-id')
|
||||
const PeerInfo = require('peer-info')
|
||||
const Node = require('./libp2p-bundle')
|
||||
const pull = require('pull-stream')
|
||||
const series = require('async/series')
|
||||
const pipe = require('it-pipe')
|
||||
|
||||
let listenerId
|
||||
let listenerNode
|
||||
async function run() {
|
||||
const listenerId = await PeerId.createFromJSON(require('./id-l'))
|
||||
|
||||
series([
|
||||
(cb) => {
|
||||
PeerId.createFromJSON(require('./id-l'), (err, id) => {
|
||||
if (err) { return cb(err) }
|
||||
listenerId = id
|
||||
cb()
|
||||
})
|
||||
},
|
||||
(cb) => {
|
||||
const listenerPeerInfo = new PeerInfo(listenerId)
|
||||
listenerPeerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/10333')
|
||||
listenerNode = new Node({
|
||||
peerInfo: listenerPeerInfo
|
||||
})
|
||||
// Listener libp2p node
|
||||
const listenerPeerInfo = new PeerInfo(listenerId)
|
||||
listenerPeerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/10333')
|
||||
const listenerNode = new Node({
|
||||
peerInfo: listenerPeerInfo
|
||||
})
|
||||
|
||||
listenerNode.on('peer:connect', (peerInfo) => {
|
||||
console.log('received dial to me from:', peerInfo.id.toB58String())
|
||||
})
|
||||
// Log a message when we receive a connection
|
||||
listenerNode.on('peer:connect', (peerInfo) => {
|
||||
console.log('received dial to me from:', peerInfo.id.toB58String())
|
||||
})
|
||||
|
||||
listenerNode.handle('/echo/1.0.0', (protocol, conn) => pull(conn, conn))
|
||||
listenerNode.start(cb)
|
||||
}
|
||||
], (err) => {
|
||||
if (err) { throw err }
|
||||
// Handle incoming connections for the protocol by piping from the stream
|
||||
// back to itself (an echo)
|
||||
await listenerNode.handle('/echo/1.0.0', ({ stream }) => pipe(stream.source, stream.sink))
|
||||
|
||||
// Start listening
|
||||
await listenerNode.start()
|
||||
|
||||
console.log('Listener ready, listening on:')
|
||||
listenerNode.peerInfo.multiaddrs.forEach((ma) => {
|
||||
console.log(ma.toString() + '/p2p/' + listenerId.toB58String())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
run()
|
||||
|
@ -17,7 +17,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"detect-dom-ready": "^1.0.2",
|
||||
"libp2p": "../../../",
|
||||
"libp2p-bootstrap": "~0.9.7",
|
||||
"libp2p-gossipsub": "~0.0.4",
|
||||
"libp2p-kad-dht": "^0.15.3",
|
||||
"libp2p-mplex": "~0.8.5",
|
||||
"libp2p-secio": "~0.11.1",
|
||||
"libp2p-spdy": "~0.13.3",
|
||||
|
@ -8,8 +8,8 @@ const SPDY = require('libp2p-spdy')
|
||||
const SECIO = require('libp2p-secio')
|
||||
const Bootstrap = require('libp2p-bootstrap')
|
||||
const DHT = require('libp2p-kad-dht')
|
||||
const defaultsDeep = require('@nodeutils/defaults-deep')
|
||||
const libp2p = require('../../../../')
|
||||
const Gossipsub = require('libp2p-gossipsub')
|
||||
const libp2p = require('libp2p')
|
||||
|
||||
// Find this list at: https://github.com/ipfs/js-ipfs/blob/master/src/core/runtime/config-browser.json
|
||||
const bootstrapList = [
|
||||
@ -26,9 +26,9 @@ const bootstrapList = [
|
||||
]
|
||||
|
||||
class Node extends libp2p {
|
||||
constructor (_options) {
|
||||
const wrtcStar = new WebRTCStar({ id: _options.peerInfo.id })
|
||||
const wsstar = new WebSocketStar({ id: _options.peerInfo.id })
|
||||
constructor ({ peerInfo }) {
|
||||
const wrtcStar = new WebRTCStar({ id: peerInfo.id })
|
||||
const wsstar = new WebSocketStar({ id: peerInfo.id })
|
||||
|
||||
const defaults = {
|
||||
modules: {
|
||||
@ -49,7 +49,8 @@ class Node extends libp2p {
|
||||
wsstar.discovery,
|
||||
Bootstrap
|
||||
],
|
||||
dht: DHT
|
||||
dht: DHT,
|
||||
pubsub: Gossipsub
|
||||
},
|
||||
config: {
|
||||
peerDiscovery: {
|
||||
@ -76,8 +77,8 @@ class Node extends libp2p {
|
||||
dht: {
|
||||
enabled: false
|
||||
},
|
||||
EXPERIMENTAL: {
|
||||
pubsub: false
|
||||
pubsub: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
connectionManager: {
|
||||
@ -86,7 +87,7 @@ class Node extends libp2p {
|
||||
}
|
||||
}
|
||||
|
||||
super(defaultsDeep(_options, defaults))
|
||||
super({ ...defaults, peerInfo })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,9 +10,11 @@ function createNode (callback) {
|
||||
}
|
||||
|
||||
const peerIdStr = peerInfo.id.toB58String()
|
||||
const ma = `/dns4/star-signal.cloud.ipfs.team/tcp/443/wss/p2p-webrtc-star/p2p/${peerIdStr}`
|
||||
const webrtcAddr = `/dns4/star-signal.cloud.ipfs.team/tcp/443/wss/p2p-webrtc-star/p2p/${peerIdStr}`
|
||||
const wsAddr = `/dns4/ws-star.discovery.libp2p.io/tcp/443/wss/p2p-websocket-star`
|
||||
|
||||
peerInfo.multiaddrs.add(ma)
|
||||
peerInfo.multiaddrs.add(webrtcAddr)
|
||||
peerInfo.multiaddrs.add(wsAddr)
|
||||
|
||||
const node = new Node({
|
||||
peerInfo
|
||||
|
@ -46,6 +46,9 @@ domReady(() => {
|
||||
myPeerDiv.append(idDiv)
|
||||
|
||||
console.log('Node is listening o/')
|
||||
node.peerInfo.multiaddrs.toArray().forEach(ma => {
|
||||
console.log(ma.toString())
|
||||
})
|
||||
|
||||
// NOTE: to stop the node
|
||||
// node.stop((err) => {})
|
||||
|
@ -20,5 +20,3 @@ Then simply go into the folder [1](./1) and execute the following
|
||||
> npm start
|
||||
# open your browser in port :9090
|
||||
```
|
||||
|
||||
[Version Published on IPFS](http://ipfs.io/ipfs/Qmbc1J7ehw1dNYachbkCWPto4RsnVvqCKNVzmYEod2gXcy)
|
||||
|
1
examples/pnet-ipfs/.gitignore
vendored
Normal file
1
examples/pnet-ipfs/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
tmp/
|
29
examples/pnet-ipfs/README.md
Normal file
29
examples/pnet-ipfs/README.md
Normal file
@ -0,0 +1,29 @@
|
||||
# Private Networking with IPFS
|
||||
This example shows how to set up a private network of IPFS nodes.
|
||||
|
||||
## Setup
|
||||
Install dependencies:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
## Run
|
||||
Running the example will cause two nodes with the same swarm key to be started and exchange basic information.
|
||||
|
||||
```
|
||||
node index.js
|
||||
```
|
||||
|
||||
### Using different keys
|
||||
This example includes `TASK` comments that can be used to try the example with different swarm keys. This will
|
||||
allow you to see how nodes will fail to connect if they are on different private networks and try to connect to
|
||||
one another.
|
||||
|
||||
To change the swarm key of one of the nodes, look through `index.js` for comments starting with `TASK` to indicate
|
||||
where lines are that pertain to changing the swarm key of node 2.
|
||||
|
||||
### Exploring the repos
|
||||
Once you've run the example you can take a look at the repos in the `./tmp` directory to see how they differ, including
|
||||
the swarm keys. You should see a `swarm.key` file in each of the repos and when the nodes are on the same private network
|
||||
this contents of the `swarm.key` files should be the same.
|
145
examples/pnet-ipfs/index.js
Normal file
145
examples/pnet-ipfs/index.js
Normal file
@ -0,0 +1,145 @@
|
||||
/* eslint no-console: ["off"] */
|
||||
'use strict'
|
||||
|
||||
const IPFS = require('ipfs')
|
||||
const assert = require('assert').strict
|
||||
const { generate: writeKey } = require('libp2p/src/pnet')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const privateLibp2pBundle = require('./libp2p-bundle')
|
||||
const { mkdirp } = require('./utils')
|
||||
|
||||
// Create two separate repo paths so we can run two nodes and check their output
|
||||
const repo1 = path.resolve('./tmp', 'repo1', '.ipfs')
|
||||
const repo2 = path.resolve('./tmp', 'repo2', '.ipfs')
|
||||
mkdirp(repo1)
|
||||
mkdirp(repo2)
|
||||
|
||||
// Create a buffer and write the swarm key to it
|
||||
const swarmKey = Buffer.alloc(95)
|
||||
writeKey(swarmKey)
|
||||
|
||||
// This key is for the `TASK` mentioned in the writeFileSync calls below
|
||||
const otherSwarmKey = Buffer.alloc(95)
|
||||
writeKey(otherSwarmKey)
|
||||
|
||||
// Add the swarm key to both repos
|
||||
const swarmKey1Path = path.resolve(repo1, 'swarm.key')
|
||||
const swarmKey2Path = path.resolve(repo2, 'swarm.key')
|
||||
fs.writeFileSync(swarmKey1Path, swarmKey)
|
||||
// TASK: switch the commented out line below so we're using a different key, to see the nodes fail to connect
|
||||
fs.writeFileSync(swarmKey2Path, swarmKey)
|
||||
// fs.writeFileSync(swarmKey2Path, otherSwarmKey)
|
||||
|
||||
// Create the first ipfs node
|
||||
const node1 = new IPFS({
|
||||
repo: repo1,
|
||||
libp2p: privateLibp2pBundle(swarmKey1Path),
|
||||
config: {
|
||||
Addresses: {
|
||||
// Set the swarm address so we dont get port collision on the nodes
|
||||
Swarm: ['/ip4/0.0.0.0/tcp/9101']
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Create the second ipfs node
|
||||
const node2 = new IPFS({
|
||||
repo: repo2,
|
||||
libp2p: privateLibp2pBundle(swarmKey2Path),
|
||||
config: {
|
||||
Addresses: {
|
||||
// Set the swarm address so we dont get port collision on the nodes
|
||||
Swarm: ['/ip4/0.0.0.0/tcp/9102']
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
console.log('auto starting the nodes...')
|
||||
|
||||
// `nodesStarted` keeps track of how many of our nodes have started
|
||||
let nodesStarted = 0
|
||||
/**
|
||||
* Calls `connectAndTalk` when both nodes have started
|
||||
* @returns {void}
|
||||
*/
|
||||
const didStartHandler = () => {
|
||||
if (++nodesStarted === 2) {
|
||||
// If both nodes are up, start talking
|
||||
connectAndTalk()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exits the process when all started nodes have stopped
|
||||
* @returns {void}
|
||||
*/
|
||||
const didStopHandler = () => {
|
||||
if (--nodesStarted < 1) {
|
||||
console.log('all nodes stopped, exiting.')
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the running nodes
|
||||
* @param {Error} err An optional error to log to the console
|
||||
* @returns {void}
|
||||
*/
|
||||
const doStop = (err) => {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
console.log('Shutting down...')
|
||||
node1.stop()
|
||||
node2.stop()
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects the IPFS nodes and transfers data between them
|
||||
* @returns {void}
|
||||
*/
|
||||
const connectAndTalk = async () => {
|
||||
console.log('connecting the nodes...')
|
||||
const node2Id = await node2.id()
|
||||
const dataToAdd = Buffer.from('Hello, private friend!')
|
||||
|
||||
// Connect the nodes
|
||||
// This will error when different private keys are used
|
||||
try {
|
||||
await node1.swarm.connect(node2Id.addresses[0])
|
||||
} catch (err) {
|
||||
return doStop(err)
|
||||
}
|
||||
console.log('the nodes are connected, let\'s add some data')
|
||||
|
||||
// Add some data to node 1
|
||||
let addedCID
|
||||
try {
|
||||
addedCID = await node1.add(dataToAdd)
|
||||
} catch (err) {
|
||||
return doStop(err)
|
||||
}
|
||||
console.log(`added ${addedCID[0].path} to the node1`)
|
||||
|
||||
// Retrieve the data from node 2
|
||||
let cattedData
|
||||
try {
|
||||
cattedData = await node2.cat(addedCID[0].path)
|
||||
} catch (err) {
|
||||
return doStop(err)
|
||||
}
|
||||
assert.deepEqual(cattedData.toString(), dataToAdd.toString(), 'Should have equal data')
|
||||
console.log(`successfully retrieved "${dataToAdd.toString()}" from node2`)
|
||||
|
||||
doStop()
|
||||
}
|
||||
|
||||
// Wait for the nodes to boot
|
||||
node1.once('start', didStartHandler)
|
||||
node2.once('start', didStartHandler)
|
||||
|
||||
// Listen for the nodes stopping so we can cleanup
|
||||
node1.once('stop', didStopHandler)
|
||||
node2.once('stop', didStopHandler)
|
60
examples/pnet-ipfs/libp2p-bundle.js
Normal file
60
examples/pnet-ipfs/libp2p-bundle.js
Normal file
@ -0,0 +1,60 @@
|
||||
'use strict'
|
||||
|
||||
const Libp2p = require('libp2p')
|
||||
const TCP = require('libp2p-tcp')
|
||||
const MPLEX = require('libp2p-mplex')
|
||||
const SECIO = require('libp2p-secio')
|
||||
const fs = require('fs')
|
||||
const Protector = require('libp2p/src/pnet')
|
||||
|
||||
/**
|
||||
* Options for the libp2p bundle
|
||||
* @typedef {Object} libp2pBundle~options
|
||||
* @property {PeerInfo} peerInfo - The PeerInfo of the IPFS node
|
||||
* @property {PeerBook} peerBook - The PeerBook of the IPFS node
|
||||
* @property {Object} config - The config of the IPFS node
|
||||
* @property {Object} options - The options given to the IPFS node
|
||||
*/
|
||||
|
||||
/**
|
||||
* privateLibp2pBundle returns a libp2p bundle function that will use the swarm
|
||||
* key at the given `swarmKeyPath` to create the Protector
|
||||
*
|
||||
* @param {string} swarmKeyPath The path to our swarm key
|
||||
* @returns {libp2pBundle} Returns a libp2pBundle function for use in IPFS creation
|
||||
*/
|
||||
const privateLibp2pBundle = (swarmKeyPath) => {
|
||||
/**
|
||||
* This is the bundle we will use to create our fully customized libp2p bundle.
|
||||
*
|
||||
* @param {libp2pBundle~options} opts The options to use when generating the libp2p node
|
||||
* @returns {Libp2p} Our new libp2p node
|
||||
*/
|
||||
const libp2pBundle = (opts) => {
|
||||
// Set convenience variables to clearly showcase some of the useful things that are available
|
||||
const peerInfo = opts.peerInfo
|
||||
const peerBook = opts.peerBook
|
||||
|
||||
// Build and return our libp2p node
|
||||
return new Libp2p({
|
||||
peerInfo,
|
||||
peerBook,
|
||||
modules: {
|
||||
transport: [TCP], // We're only using the TCP transport for this example
|
||||
streamMuxer: [MPLEX], // We're only using mplex muxing
|
||||
// Let's make sure to use identifying crypto in our pnet since the protector doesn't
|
||||
// care about node identity, and only the presence of private keys
|
||||
connEncryption: [SECIO],
|
||||
// Leave peer discovery empty, we don't want to find peers. We could omit the property, but it's
|
||||
// being left in for explicit readability.
|
||||
// We should explicitly dial pnet peers, or use a custom discovery service for finding nodes in our pnet
|
||||
peerDiscovery: [],
|
||||
connProtector: new Protector(fs.readFileSync(swarmKeyPath))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return libp2pBundle
|
||||
}
|
||||
|
||||
module.exports = privateLibp2pBundle
|
20
examples/pnet-ipfs/package.json
Normal file
20
examples/pnet-ipfs/package.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "pnet-ipfs-example",
|
||||
"version": "1.0.0",
|
||||
"description": "An example of private networking with IPFS",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node index.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"ipfs": "^0.38.0",
|
||||
"libp2p": "^0.26.2",
|
||||
"libp2p-mplex": "^0.8.5",
|
||||
"libp2p-secio": "^0.11.1",
|
||||
"libp2p-tcp": "^0.13.2"
|
||||
}
|
||||
}
|
28
examples/pnet-ipfs/utils.js
Normal file
28
examples/pnet-ipfs/utils.js
Normal file
@ -0,0 +1,28 @@
|
||||
'use strict'
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
/**
|
||||
* mkdirp recursively creates needed folders for the given dir path
|
||||
* @param {string} dir
|
||||
* @returns {string} The path that was created
|
||||
*/
|
||||
module.exports.mkdirp = (dir) => {
|
||||
return path
|
||||
.resolve(dir)
|
||||
.split(path.sep)
|
||||
.reduce((acc, cur) => {
|
||||
const currentPath = path.normalize(acc + path.sep + cur)
|
||||
|
||||
try {
|
||||
fs.statSync(currentPath)
|
||||
} catch (e) {
|
||||
if (e.code === 'ENOENT') {
|
||||
fs.mkdirSync(currentPath)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
return currentPath
|
||||
}, '')
|
||||
}
|
@ -7,6 +7,7 @@ const Mplex = require('libp2p-mplex')
|
||||
const SECIO = require('libp2p-secio')
|
||||
const PeerInfo = require('peer-info')
|
||||
const MulticastDNS = require('libp2p-mdns')
|
||||
const Gossipsub = require('libp2p-gossipsub')
|
||||
const defaultsDeep = require('@nodeutils/defaults-deep')
|
||||
const waterfall = require('async/waterfall')
|
||||
const parallel = require('async/parallel')
|
||||
@ -19,7 +20,8 @@ class MyBundle extends libp2p {
|
||||
transport: [ TCP ],
|
||||
streamMuxer: [ Mplex ],
|
||||
connEncryption: [ SECIO ],
|
||||
peerDiscovery: [ MulticastDNS ]
|
||||
peerDiscovery: [ MulticastDNS ],
|
||||
pubsub: Gossipsub
|
||||
},
|
||||
config: {
|
||||
peerDiscovery: {
|
||||
@ -28,8 +30,9 @@ class MyBundle extends libp2p {
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
EXPERIMENTAL: {
|
||||
pubsub: true
|
||||
pubsub: {
|
||||
enabled: true,
|
||||
emitSelf: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -65,19 +68,36 @@ parallel([
|
||||
node1.once('peer:connect', (peer) => {
|
||||
console.log('connected to %s', peer.id.toB58String())
|
||||
|
||||
// Subscribe to the topic 'news'
|
||||
node1.pubsub.subscribe('news',
|
||||
(msg) => console.log(msg.from, msg.data.toString()),
|
||||
() => {
|
||||
series([
|
||||
// node1 subscribes to "news"
|
||||
(cb) => node1.pubsub.subscribe(
|
||||
'news',
|
||||
(msg) => console.log(`node1 received: ${msg.data.toString()}`),
|
||||
cb
|
||||
),
|
||||
(cb) => setTimeout(cb, 500),
|
||||
// node2 subscribes to "news"
|
||||
(cb) => node2.pubsub.subscribe(
|
||||
'news',
|
||||
(msg) => console.log(`node2 received: ${msg.data.toString()}`),
|
||||
cb
|
||||
),
|
||||
(cb) => setTimeout(cb, 500),
|
||||
// node2 publishes "news" every second
|
||||
(cb) => {
|
||||
setInterval(() => {
|
||||
// Publish the message on topic 'news'
|
||||
node2.pubsub.publish(
|
||||
'news',
|
||||
Buffer.from('Bird bird bird, bird is the word!'),
|
||||
() => {}
|
||||
(err) => {
|
||||
if (err) { throw err }
|
||||
}
|
||||
)
|
||||
}, 1000)
|
||||
}
|
||||
)
|
||||
cb()
|
||||
},
|
||||
], (err) => {
|
||||
if (err) { throw err }
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Publish Subscribe
|
||||
|
||||
Publish Subscribe is also included on the stack. Currently, we have on PubSub implementation which we ship by default [libp2p-floodsub](https://github.com/libp2p/js-libp2p-floodsub), with many more being researched at [research-pubsub](https://github.com/libp2p/research-pubsub).
|
||||
Publish Subscribe is also included on the stack. Currently, we have two PubSub implementation available [libp2p-floodsub](https://github.com/libp2p/js-libp2p-floodsub) and [libp2p-gossipsub](https://github.com/ChainSafe/gossipsub-js), with many more being researched at [research-pubsub](https://github.com/libp2p/research-pubsub).
|
||||
|
||||
We've seen many interesting use cases appear with this, here are some highlights:
|
||||
|
||||
@ -12,26 +12,43 @@ We've seen many interesting use cases appear with this, here are some highlights
|
||||
|
||||
For this example, we will use MulticastDNS for automatic Peer Discovery. This example is based the previous examples found in [Discovery Mechanisms](../discovery-mechanisms). You can find the complete version at [1.js](./1.js).
|
||||
|
||||
Using PubSub is super simple, all you have to do is start a libp2p node with `EXPERIMENTAL.pubsub` set to true.
|
||||
Using PubSub is super simple, you only need to provide the implementation of your choice and you are ready to go. No need for extra configuration.
|
||||
|
||||
```JavaScript
|
||||
node1.once('peer:connect', (peer) => {
|
||||
console.log('connected to %s', peer.id.toB58String())
|
||||
|
||||
// Subscribe to the topic 'news'
|
||||
node1.pubsub.subscribe('news',
|
||||
(msg) => console.log(msg.from, msg.data.toString()),
|
||||
() => {
|
||||
series([
|
||||
// node1 subscribes to "news"
|
||||
(cb) => node1.pubsub.subscribe(
|
||||
'news',
|
||||
(msg) => console.log(`node1 received: ${msg.data.toString()}`),
|
||||
cb
|
||||
),
|
||||
(cb) => setTimeout(cb, 500),
|
||||
// node2 subscribes to "news"
|
||||
(cb) => node2.pubsub.subscribe(
|
||||
'news',
|
||||
(msg) => console.log(`node2 received: ${msg.data.toString()}`),
|
||||
cb
|
||||
),
|
||||
(cb) => setTimeout(cb, 500),
|
||||
// node2 publishes "news" every second
|
||||
(cb) => {
|
||||
setInterval(() => {
|
||||
// Publish the message on topic 'news'
|
||||
node2.pubsub.publish(
|
||||
'news',
|
||||
Buffer.from('Bird bird bird, bird is the word!'),
|
||||
() => {}
|
||||
(err) => {
|
||||
if (err) { throw err }
|
||||
}
|
||||
)
|
||||
}, 1000)
|
||||
}
|
||||
)
|
||||
cb()
|
||||
},
|
||||
], (err) => {
|
||||
if (err) { throw err }
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
@ -40,11 +57,29 @@ The output of the program should look like:
|
||||
```
|
||||
> node 1.js
|
||||
connected to QmWpvkKm6qHLhoxpWrTswY6UMNWDyn8hN265Qp9ZYvgS82
|
||||
QmWpvkKm6qHLhoxpWrTswY6UMNWDyn8hN265Qp9ZYvgS82 Bird bird bird, bird is the word!
|
||||
QmWpvkKm6qHLhoxpWrTswY6UMNWDyn8hN265Qp9ZYvgS82 Bird bird bird, bird is the word!
|
||||
QmWpvkKm6qHLhoxpWrTswY6UMNWDyn8hN265Qp9ZYvgS82 Bird bird bird, bird is the word!
|
||||
QmWpvkKm6qHLhoxpWrTswY6UMNWDyn8hN265Qp9ZYvgS82 Bird bird bird, bird is the word!
|
||||
QmWpvkKm6qHLhoxpWrTswY6UMNWDyn8hN265Qp9ZYvgS82 Bird bird bird, bird is the word!
|
||||
node2 received: Bird bird bird, bird is the word!
|
||||
node1 received: Bird bird bird, bird is the word!
|
||||
node2 received: Bird bird bird, bird is the word!
|
||||
node1 received: Bird bird bird, bird is the word!
|
||||
```
|
||||
|
||||
You can change the pubsub `emitSelf` option if you don't want the publishing node to receive its own messages.
|
||||
|
||||
```JavaScript
|
||||
const defaults = {
|
||||
config: {
|
||||
peerDiscovery: {
|
||||
mdns: {
|
||||
interval: 2000,
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
pubsub: {
|
||||
enabled: true,
|
||||
emitSelf: false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2. Future work
|
||||
|
@ -3,40 +3,34 @@
|
||||
"Package",
|
||||
"Version",
|
||||
"Deps",
|
||||
"CI/Travis",
|
||||
"CI",
|
||||
"Coverage",
|
||||
"Lead Maintainer"
|
||||
],
|
||||
"rows": [
|
||||
"Libp2p",
|
||||
["libp2p/interface-libp2p", "interface-libp2p"],
|
||||
"libp2p",
|
||||
["libp2p/js-libp2p", "libp2p"],
|
||||
["libp2p/js-libp2p-daemon", "libp2p-daemon"],
|
||||
["libp2p/js-libp2p-daemon-client", "libp2p-daemon-client"],
|
||||
["libp2p/js-interfaces", "libp2p-interfaces"],
|
||||
["libp2p/interop", "interop-libp2p"],
|
||||
|
||||
"Connection",
|
||||
["libp2p/interface-connection", "interface-connection"],
|
||||
|
||||
"Transport",
|
||||
["libp2p/interface-transport", "interface-transport"],
|
||||
"transports",
|
||||
["libp2p/js-libp2p-tcp", "libp2p-tcp"],
|
||||
["libp2p/js-libp2p-udp", "libp2p-udp"],
|
||||
["libp2p/js-libp2p-udt", "libp2p-udt"],
|
||||
["libp2p/js-libp2p-utp", "libp2p-utp"],
|
||||
["libp2p/js-libp2p-webrtc-direct", "libp2p-webrtc-direct"],
|
||||
["libp2p/js-libp2p-webrtc-star", "libp2p-webrtc-star"],
|
||||
["libp2p/js-libp2p-websockets", "libp2p-websockets"],
|
||||
["libp2p/js-libp2p-websocket-star", "libp2p-websocket-star"],
|
||||
["libp2p/js-libp2p-websocket-star-rendezvous", "libp2p-websocket-star-rendezvous"],
|
||||
|
||||
"Crypto Channels",
|
||||
"secure channels",
|
||||
["libp2p/js-libp2p-secio", "libp2p-secio"],
|
||||
|
||||
"Stream Muxers",
|
||||
["libp2p/interface-stream-muxer", "interface-stream-muxer"],
|
||||
"stream multiplexers",
|
||||
["libp2p/js-libp2p-mplex", "libp2p-mplex"],
|
||||
["libp2p/js-libp2p-spdy", "libp2p-spdy"],
|
||||
|
||||
"Discovery",
|
||||
["libp2p/interface-peer-discovery", "interface-peer-discovery"],
|
||||
"peer discovery",
|
||||
["libp2p/js-libp2p-bootstrap", "libp2p-bootstrap"],
|
||||
["libp2p/js-libp2p-kad-dht", "libp2p-kad-dht"],
|
||||
["libp2p/js-libp2p-mdns", "libp2p-mdns"],
|
||||
@ -44,43 +38,29 @@
|
||||
["libp2p/js-libp2p-webrtc-star", "libp2p-webrtc-star"],
|
||||
["libp2p/js-libp2p-websocket-star", "libp2p-websocket-star"],
|
||||
|
||||
"NAT Traversal",
|
||||
["libp2p/js-libp2p-circuit", "libp2p-circuit"],
|
||||
["libp2p/js-libp2p-nat-mngr", "libp2p-nat-mngr"],
|
||||
|
||||
"Data Types",
|
||||
["libp2p/js-peer-book", "peer-book"],
|
||||
["libp2p/js-peer-id", "peer-id"],
|
||||
["libp2p/js-peer-info", "peer-info"],
|
||||
|
||||
"Content Routing",
|
||||
["libp2p/interface-content-routing", "interface-content-routing"],
|
||||
"content routing",
|
||||
["libp2p/js-libp2p-delegated-content-routing", "libp2p-delegated-content-routing"],
|
||||
["libp2p/js-libp2p-kad-dht", "libp2p-kad-dht"],
|
||||
|
||||
"Peer Routing",
|
||||
["libp2p/interface-peer-routing", "interface-peer-routing"],
|
||||
"peer routing",
|
||||
["libp2p/js-libp2p-delegated-peer-routing", "libp2p-delegated-peer-routing"],
|
||||
["libp2p/js-libp2p-kad-dht", "libp2p-kad-dht"],
|
||||
|
||||
"Record Store",
|
||||
["libp2p/interface-record-store", "interface-record-store"],
|
||||
["libp2p/js-libp2p-record", "libp2p-record"],
|
||||
|
||||
"Generics",
|
||||
["libp2p/js-libp2p-connection-manager", "libp2p-connection-manager"],
|
||||
"utilities",
|
||||
["libp2p/js-libp2p-crypto", "libp2p-crypto"],
|
||||
["libp2p/js-libp2p-crypto-secp256k1", "libp2p-crypto-secp256k1"],
|
||||
["libp2p/js-libp2p-switch", "libp2p-switch"],
|
||||
|
||||
"Extensions",
|
||||
"data types",
|
||||
["libp2p/js-peer-id", "peer-id"],
|
||||
["libp2p/js-peer-info", "peer-info"],
|
||||
|
||||
"pubsub",
|
||||
["libp2p/js-libp2p-pubsub", "libp2p-pubsub"],
|
||||
["libp2p/js-libp2p-floodsub", "libp2p-floodsub"],
|
||||
["libp2p/js-libp2p-identify", "libp2p-identify"],
|
||||
["libp2p/js-libp2p-keychain", "libp2p-keychain"],
|
||||
["libp2p/js-libp2p-ping", "libp2p-ping"],
|
||||
["libp2p/js-libp2p-pnet", "libp2p-pnet"],
|
||||
["ChainSafe/gossipsub-js", "libp2p-gossipsub"],
|
||||
|
||||
"Utilities",
|
||||
["libp2p/js-p2pcat", "p2pcat"]
|
||||
"extensions",
|
||||
["libp2p/js-libp2p-nat-mgnr", "libp2p-nat-mgnr"],
|
||||
["libp2p/js-libp2p-utils", "libp2p-utils"]
|
||||
]
|
||||
}
|
||||
|
110
package.json
110
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "libp2p",
|
||||
"version": "0.25.4",
|
||||
"version": "0.27.0-pre.1",
|
||||
"description": "JavaScript implementation of libp2p, a modular peer to peer network stack",
|
||||
"leadMaintainer": "Jacob Heun <jacobheun@gmail.com>",
|
||||
"main": "src/index.js",
|
||||
@ -11,12 +11,13 @@
|
||||
"scripts": {
|
||||
"lint": "aegir lint",
|
||||
"build": "aegir build",
|
||||
"test": "aegir test -t node -t browser",
|
||||
"test:node": "aegir test -t node",
|
||||
"test": "npm run test:node && npm run test:browser",
|
||||
"test:node": "aegir test -t node -f \"./test/**/*.{node,spec}.js\"",
|
||||
"test:browser": "aegir test -t browser",
|
||||
"release": "aegir release -t node -t browser",
|
||||
"release-minor": "aegir release --type minor -t node -t browser",
|
||||
"release-major": "aegir release --type major -t node -t browser"
|
||||
"release-major": "aegir release --type major -t node -t browser",
|
||||
"coverage": "nyc --reporter=text --reporter=lcov npm run test:node"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -35,74 +36,91 @@
|
||||
},
|
||||
"homepage": "https://libp2p.io",
|
||||
"license": "MIT",
|
||||
"browser": {
|
||||
"./test/utils/bundle-nodejs": "./test/utils/bundle-browser"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0",
|
||||
"npm": ">=6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"async": "^2.6.2",
|
||||
"abort-controller": "^3.0.0",
|
||||
"aggregate-error": "^3.0.1",
|
||||
"any-signal": "^1.1.0",
|
||||
"bignumber.js": "^9.0.0",
|
||||
"class-is": "^1.1.0",
|
||||
"debug": "^4.1.1",
|
||||
"err-code": "^1.1.2",
|
||||
"fsm-event": "^2.1.0",
|
||||
"libp2p-connection-manager": "^0.1.0",
|
||||
"libp2p-floodsub": "^0.16.1",
|
||||
"libp2p-ping": "^0.8.5",
|
||||
"libp2p-switch": "^0.42.12",
|
||||
"libp2p-websockets": "^0.12.2",
|
||||
"mafmt": "^6.0.7",
|
||||
"multiaddr": "^6.1.0",
|
||||
"once": "^1.4.0",
|
||||
"peer-book": "^0.9.1",
|
||||
"peer-id": "^0.12.2",
|
||||
"peer-info": "^0.15.1",
|
||||
"superstruct": "^0.6.0"
|
||||
"hashlru": "^2.3.0",
|
||||
"it-all": "^1.0.1",
|
||||
"it-buffer": "^0.1.1",
|
||||
"it-handshake": "^1.0.1",
|
||||
"it-length-prefixed": "^3.0.0",
|
||||
"it-pipe": "^1.1.0",
|
||||
"it-protocol-buffers": "^0.2.0",
|
||||
"latency-monitor": "~0.2.1",
|
||||
"libp2p-crypto": "^0.17.1",
|
||||
"libp2p-interfaces": "^0.1.5",
|
||||
"mafmt": "^7.0.0",
|
||||
"merge-options": "^1.0.1",
|
||||
"moving-average": "^1.0.0",
|
||||
"multiaddr": "^7.2.1",
|
||||
"multistream-select": "^0.15.0",
|
||||
"mutable-proxy": "^1.0.0",
|
||||
"p-any": "^2.1.0",
|
||||
"p-fifo": "^1.0.0",
|
||||
"p-settle": "^3.1.0",
|
||||
"peer-id": "^0.13.4",
|
||||
"peer-info": "^0.17.0",
|
||||
"protons": "^1.0.1",
|
||||
"retimer": "^2.0.0",
|
||||
"timeout-abort-controller": "^1.0.0",
|
||||
"xsalsa20": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nodeutils/defaults-deep": "^1.1.0",
|
||||
"aegir": "^19.0.3",
|
||||
"abortable-iterator": "^2.1.0",
|
||||
"aegir": "^20.4.1",
|
||||
"chai": "^4.2.0",
|
||||
"chai-checkmark": "^1.0.1",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"cids": "^0.7.1",
|
||||
"delay": "^4.3.0",
|
||||
"dirty-chai": "^2.0.1",
|
||||
"electron-webrtc": "^0.3.0",
|
||||
"interface-datastore": "^0.6.0",
|
||||
"libp2p-bootstrap": "^0.9.7",
|
||||
"libp2p-circuit": "^0.3.7",
|
||||
"libp2p-delegated-content-routing": "^0.2.2",
|
||||
"libp2p-delegated-peer-routing": "^0.2.2",
|
||||
"libp2p-kad-dht": "^0.15.2",
|
||||
"libp2p-mdns": "^0.12.3",
|
||||
"libp2p-mplex": "^0.8.4",
|
||||
"libp2p-secio": "^0.11.1",
|
||||
"libp2p-spdy": "^0.13.2",
|
||||
"libp2p-tcp": "^0.13.0",
|
||||
"libp2p-webrtc-star": "^0.16.1",
|
||||
"libp2p-websocket-star": "~0.10.2",
|
||||
"libp2p-websocket-star-rendezvous": "~0.3.0",
|
||||
"lodash.times": "^4.3.2",
|
||||
"it-concat": "^1.0.0",
|
||||
"it-pair": "^1.0.0",
|
||||
"it-pushable": "^1.4.0",
|
||||
"libp2p-bootstrap": "^0.10.3",
|
||||
"libp2p-delegated-content-routing": "^0.4.1",
|
||||
"libp2p-delegated-peer-routing": "^0.4.0",
|
||||
"libp2p-floodsub": "^0.20.0",
|
||||
"libp2p-gossipsub": "^0.2.0",
|
||||
"libp2p-kad-dht": "^0.18.2",
|
||||
"libp2p-mdns": "^0.13.0",
|
||||
"libp2p-mplex": "^0.9.1",
|
||||
"libp2p-secio": "^0.12.1",
|
||||
"libp2p-tcp": "^0.14.1",
|
||||
"libp2p-webrtc-star": "^0.17.0",
|
||||
"libp2p-websockets": "^0.13.1",
|
||||
"nock": "^10.0.6",
|
||||
"pull-goodbye": "0.0.2",
|
||||
"pull-mplex": "^0.1.2",
|
||||
"pull-serializer": "^0.3.2",
|
||||
"pull-stream": "^3.6.12",
|
||||
"p-defer": "^3.0.0",
|
||||
"p-times": "^2.1.0",
|
||||
"p-wait-for": "^3.1.0",
|
||||
"sinon": "^7.2.7",
|
||||
"streaming-iterables": "^4.1.0",
|
||||
"wrtc": "^0.4.1"
|
||||
},
|
||||
"contributors": [
|
||||
"Aditya Bose <13054902+adbose@users.noreply.github.com>",
|
||||
"Alan Shaw <alan.shaw@protocol.ai>",
|
||||
"Alan Shaw <alan@tableflip.io>",
|
||||
"Alex Potsides <alex@achingbrain.net>",
|
||||
"Andrew Nesbitt <andrewnez@gmail.com>",
|
||||
"Chris Bratlien <chrisbratlien@gmail.com>",
|
||||
"Chris Dostert <chrisdostert@users.noreply.github.com>",
|
||||
"Daijiro Wachi <daijiro.wachi@gmail.com>",
|
||||
"David Dias <daviddias.p@gmail.com>",
|
||||
"Didrik Nordström <didrik.nordstrom@gmail.com>",
|
||||
"Diogo Silva <fsdiogo@gmail.com>",
|
||||
"Dmitriy Ryajov <dryajov@gmail.com>",
|
||||
"Elven <mon.samuel@qq.com>",
|
||||
"Fei Liu <liu.feiwood@gmail.com>",
|
||||
"Florian-Merle <florian.david.merle@gmail.com>",
|
||||
"Friedel Ziegelmayer <dignifiedquire@gmail.com>",
|
||||
"Giovanni T. Parra <fiatjaf@gmail.com>",
|
||||
@ -129,15 +147,19 @@
|
||||
"Sönke Hahn <soenkehahn@gmail.com>",
|
||||
"Thomas Eizinger <thomas@eizinger.io>",
|
||||
"Tiago Alves <alvesjtiago@gmail.com>",
|
||||
"Vasco Santos <vasco.santos@ua.pt>",
|
||||
"Vasco Santos <vasco.santos@moxy.studio>",
|
||||
"Vasco Santos <vasco.santos@ua.pt>",
|
||||
"Volker Mische <volker.mische@gmail.com>",
|
||||
"Yusef Napora <yusef@napora.org>",
|
||||
"Zane Starr <zcstarr@gmail.com>",
|
||||
"a1300 <a1300@users.noreply.github.com>",
|
||||
"dirkmc <dirkmdev@gmail.com>",
|
||||
"ebinks <elizabethjbinks@gmail.com>",
|
||||
"greenkeeperio-bot <support@greenkeeper.io>",
|
||||
"isan_rivkin <isanrivkin@gmail.com>",
|
||||
"mayerwin <mayerwin@users.noreply.github.com>",
|
||||
"phillmac <phillmac@users.noreply.github.com>",
|
||||
"swedneck <40505480+swedneck@users.noreply.github.com>",
|
||||
"ᴠɪᴄᴛᴏʀ ʙᴊᴇʟᴋʜᴏʟᴍ <victorbjelkholm@gmail.com>"
|
||||
]
|
||||
}
|
||||
|
128
src/circuit/IMPLEMENTATION_NOTES.md
Normal file
128
src/circuit/IMPLEMENTATION_NOTES.md
Normal file
@ -0,0 +1,128 @@
|
||||
EDIT: This document is outdated and here only for historical purposes
|
||||
|
||||
NOTE: This document is structured in an `if-then/else[if]-then` manner, each line is a precondition for following lines with a higher number of indentation
|
||||
|
||||
Example:
|
||||
|
||||
- if there are apples
|
||||
- eat them
|
||||
- if not, check for pears
|
||||
- then eat them
|
||||
- if not, check for cherries
|
||||
- then eat them
|
||||
|
||||
Or,
|
||||
|
||||
- if there are apples
|
||||
- eat them
|
||||
- if not
|
||||
- check for pears
|
||||
- then eat them
|
||||
- if not
|
||||
- check for cherries
|
||||
- then eat them
|
||||
|
||||
In order to minimize nesting, the first example is preferred
|
||||
|
||||
# Relay flow
|
||||
|
||||
## Relay transport (dialer/listener)
|
||||
|
||||
- ### Dial over a relay
|
||||
- See if there is a relay that's already connected to the destination peer, if not
|
||||
- Ask all the peer's known relays to dial the destination peer until an active relay (one that can dial on behalf of other peers), or a relay that may have recently acquired a connection to the destination peer is successful.
|
||||
- If successful
|
||||
- Write the `/ipfs/relay/circuit/1.0.0` header to the relay, followed by the destination address
|
||||
- e.g. `/ipfs/relay/circuit/1.0.0\n/p2p-circuit/ipfs/QmDest`.
|
||||
- If no relays could connect, fail the same way a regular transport would
|
||||
- Once the connection has been established, the swarm should treat it as a regular connection,
|
||||
- i.e. muxing, encrypt, etc should all be performed on the relayed connection
|
||||
|
||||
- ### Listen for relayed connections
|
||||
- Peer mounts the `/ipfs/relay/circuit/1.0.0` proto and listens for relayed connections
|
||||
- A connection arrives
|
||||
- read the address of the source peer from the incoming connection stream
|
||||
- if valid, create a PeerInfo object for that peer and add the incoming address to its multiaddresses list
|
||||
- pass the connection to `protocolMuxer(swarm.protocols, conn)` to have it go through the regular muxing/encryption flow
|
||||
|
||||
- ### Relay discovery and static relay addresses in swarm config
|
||||
|
||||
- #### Relay address in swarm config
|
||||
- A peer has relay addresses in its swarm config section
|
||||
- On node startup, connect to the relays in swarm config
|
||||
- if successful add address to swarms PeerInfo's multiaddresses
|
||||
- `identify` should take care of announcing that the peer is reachable over the listed relays
|
||||
|
||||
- #### Passive relay discovery
|
||||
- A peer that can dial over `/ipfs/relay/circuit/1.0.0` listens for the `peer-mux-established` swarm event, every time a new muxed connection arrives, it checks if the incoming peer is a relay. (How would this work? Some way of discovering if its a relay is required.)
|
||||
- *Useful in cases when the peer/node doesn't know of any relays on startup and also, to learn of as many additional relays in the network as possible*
|
||||
- *Useful during startup, when connecting to bootstrap nodes. It allows us to implicitly learn if its a relay without having to explicitly add `/p2p-circuit` addresses to the bootstrap list*
|
||||
- *Also useful if the relay communicates its capabilities upon connecting to it, as to avoid additional unnecessary requests/queries. I.e. if it supports weather its able to forward connections and weather it supports the `ls` or other commands.*
|
||||
- *Should it be possible to disable passive relay discovery?*
|
||||
- This could be useful when the peer wants to be reachable **only** over the listed relays
|
||||
- If the incoming peer is a relay, send an `ls` and record its peers
|
||||
|
||||
## Relay Nodes
|
||||
|
||||
- ### Passive relay node
|
||||
- *A passive relay does not explicitly dial into any requested peer, only those that it's swarm already has connections to.*
|
||||
- When the relay gets a request, read the the destination peer's multiaddr from the connection stream and if its a valid address and peer id
|
||||
- check its swarm's peerbook(?) see if its a known peer, if it is
|
||||
- use the swarms existing connection and
|
||||
- send the multistream header and the source peer address to the dest peer
|
||||
- e.g. `/ipfs/relay/circuit/1.0.0\n/p2p-circuit/ipfs/QmSource`
|
||||
- circuit the source and dest connections
|
||||
- if couldn't dial, or the connection/stream to the dest peer closed prematurelly
|
||||
- close the src stream
|
||||
|
||||
|
||||
- ### Active relay node
|
||||
- *An active relay node can dial other peers even if its swarm doesnt know about those peers*
|
||||
- When the relay gets a request, read the the destination peer's multiaddr from the connection stream and if its a valid address and peer id
|
||||
- use the swarm to dial to the dest node
|
||||
- send the multistream header and the source peer address to the dest peer
|
||||
- e.g. `/ipfs/relay/circuit/1.0.0\n/p2p-circuit/ipfs/QmSource`
|
||||
- circuit the source and dest connections
|
||||
- if couldn't dial, or the connection/stream to the dest peer closed prematurely
|
||||
- close the src stream
|
||||
|
||||
- ### `ls` command
|
||||
- *A relay node can allow the peers known to it's swarm to be listed*
|
||||
- *this should be possible to enable/disable from the config*
|
||||
- when a relay gets the `ls` request
|
||||
- if enabled, get its swarm's peerbook's known peers and return their ids and multiaddrs
|
||||
- e.g `[{id: /ipfs/QmPeerId, addrs: ['ma1', 'ma2', 'ma3']}, ...]`
|
||||
- if disabled, respond with `na`
|
||||
|
||||
|
||||
## Relay Implementation notes
|
||||
|
||||
- ### Relay transport
|
||||
- Currently I've implemented the dialer and listener parts of the relay as a transport, meaning that it *tries* to implement the `interface-transport` interface as closely as possible. This seems to work pretty well and it's makes the dialer/listener parts really easy to plug in into the swarm. I think this is the cleanest solution.
|
||||
|
||||
- ### `circuit-relay`
|
||||
- This is implemented as a separate piece (not a transport), and it can be enabled/disabled with a config. The transport listener however, will do the initial parsing of the incoming header and figure out weather it's a connection that's needs to be handled by the circuit-relay, or its a connection that is being relayed from a circuit-relay.
|
||||
|
||||
## Relay swarm integration
|
||||
|
||||
- The relay transport is mounted explicitly by calling the `swarm.connection.relay(config.relay)` from libp2p
|
||||
- Swarm will register the dialer and listener using the swarm `transport.add` and `transport.listen` methods
|
||||
|
||||
- ### Listener
|
||||
- the listener registers itself as a multistream handler on the `/ipfs/relay/circuit/1.0.0` proto
|
||||
- if `circuit-relay` is enabled, the listener will delegate connections to it if appropriate
|
||||
- when the listener receives a connection, it will read the multiaddr and determine if its a connection that needs to be relayed, or its a connection that is being relayed
|
||||
|
||||
- ### Dialer
|
||||
- When the swarm attempts to dial to a peer, it will filter the protocols that the peer can be reached on
|
||||
- *The relay will be used in two cases*
|
||||
- If the peer has an explicit relay address that it can be reached on
|
||||
- no other transport is available
|
||||
- The relay will attempt to dial the peer over that relay
|
||||
- If no explicit relay address is provided
|
||||
- no other transport is available
|
||||
- A generic circuit address will be added to the peers multiaddr list
|
||||
- i.e. `/p2p-circuit/ipfs/QmDest`
|
||||
- If another transport is available, then use that instead of the relay
|
||||
|
||||
|
133
src/circuit/README.md
Normal file
133
src/circuit/README.md
Normal file
@ -0,0 +1,133 @@
|
||||
# js-libp2p-circuit
|
||||
|
||||
> Node.js implementation of the Circuit module that libp2p uses, which implements the [interface-connection](https://github.com/libp2p/js-interfaces/tree/master/src/connection) interface for dial/listen.
|
||||
|
||||
**Note**: git history prior to merging into js-libp2p can be found in the original repository, https://github.com/libp2p/js-libp2p-circuit.
|
||||
|
||||
`libp2p-circuit` implements the circuit-relay mechanism that allows nodes that don't speak the same protocol to communicate using a third _relay_ node.
|
||||
|
||||
This module uses [pull-streams](https://pull-stream.github.io) for all stream based interfaces.
|
||||
|
||||
### Why?
|
||||
|
||||
`circuit-relaying` uses additional nodes in order to transfer traffic between two otherwise unreachable nodes. This allows nodes that don't speak the same protocols or are running in limited environments, e.g. browsers and IoT devices, to communicate, which would otherwise be impossible given the fact that for example browsers don't have any socket support and as such cannot be directly dialed.
|
||||
|
||||
The use of circuit-relaying is not limited to routing traffic between browser nodes, other uses include:
|
||||
- routing traffic between private nets and circumventing NAT layers
|
||||
- route mangling for better privacy (matreshka/shallot dialing).
|
||||
|
||||
It's also possible to use it for clients that implement exotic transports such as devices that only have bluetooth radios to be reachable over bluetooth enabled relays and become full p2p nodes.
|
||||
|
||||
### libp2p-circuit and IPFS
|
||||
|
||||
Prior to `libp2p-circuit` there was a rift in the IPFS network, were IPFS nodes could only access content from nodes that speak the same protocol, for example TCP only nodes could only dial to other TCP only nodes, same for any other protocol combination. In practice, this limitation was most visible in JS-IPFS browser nodes, since they can only dial out but not be dialed in over WebRTC or WebSockets, hence any content that the browser node held was not reachable by the rest of the network even through it was announced on the DHT. Non browser IPFS nodes would usually speak more than one protocol such as TCP, WebSockets and/or WebRTC, this made the problem less severe outside of the browser. `libp2p-circuit` solves this problem completely, as long as there are `relay nodes` capable of routing traffic between those nodes their content should be available to the rest of the IPFS network.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [js-libp2p-circuit](#js-libp2p-circuit)
|
||||
- [Why?](#why)
|
||||
- [libp2p-circuit and IPFS](#libp2p-circuit-and-ipfs)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Usage](#usage)
|
||||
- [Example](#example)
|
||||
- [Create dialer/listener](#create-dialerlistener)
|
||||
- [Create `relay`](#create-relay)
|
||||
- [API](#api)
|
||||
- [Implementation rational](#implementation-rational)
|
||||
|
||||
## Usage
|
||||
|
||||
### Example
|
||||
|
||||
#### Create dialer/listener
|
||||
|
||||
```js
|
||||
const Circuit = require('libp2p-circuit')
|
||||
const multiaddr = require('multiaddr')
|
||||
const pull = require('pull-stream')
|
||||
|
||||
const mh1 = multiaddr('/p2p-circuit/p2p/QmHash') // dial /p2p/QmHash over any circuit
|
||||
|
||||
const circuit = new Circuit(swarmInstance, options) // pass swarm instance and options
|
||||
|
||||
const listener = circuit.createListener(mh1, (connection) => {
|
||||
console.log('new connection opened')
|
||||
pull(
|
||||
pull.values(['hello']),
|
||||
socket
|
||||
)
|
||||
})
|
||||
|
||||
listener.listen(() => {
|
||||
console.log('listening')
|
||||
|
||||
pull(
|
||||
circuit.dial(mh1),
|
||||
pull.log,
|
||||
pull.onEnd(() => {
|
||||
circuit.close()
|
||||
})
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
Outputs:
|
||||
|
||||
```sh
|
||||
listening
|
||||
new connection opened
|
||||
hello
|
||||
```
|
||||
|
||||
#### Create `relay`
|
||||
|
||||
```js
|
||||
const Relay = require('libp2p-circuit').Relay
|
||||
|
||||
const relay = new Relay(options)
|
||||
|
||||
relay.mount(swarmInstance) // start relaying traffic
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
[](https://github.com/libp2p/interface-transport)
|
||||
|
||||
`libp2p-circuit` accepts Circuit addresses for both IPFS and non IPFS encapsulated addresses, i.e:
|
||||
|
||||
`/p2p-circuit/ip4/127.0.0.1/tcp/4001/p2p/QmHash`
|
||||
|
||||
Both for dialing and listening.
|
||||
|
||||
### Implementation rational
|
||||
|
||||
This module is not a transport, however it implements `interface-transport` interface in order to allow circuit to be plugged with `libp2p-swarm`. The rational behind it is that, `libp2p-circuit` has a dial and listen flow, which fits nicely with other transports, moreover, it requires the _raw_ connection to be encrypted and muxed just as a regular transport's connection does. All in all, `interface-transport` ended up being the correct level of abstraction for circuit, as well as allowed us to reuse existing integration points in `libp2p-swarm` and `libp2p` without adding any ad-hoc logic. All parts of `interface-transport` are used, including `.getAddr` which returns a list of `/p2p-circuit` addresses that circuit is currently listening.
|
||||
|
||||
```
|
||||
libp2p libp2p-circuit (transport)
|
||||
+-------------------------------------------------+ +--------------------------+
|
||||
| +---------------------------------+ | | |
|
||||
| | | | | +------------------+ |
|
||||
| | | | circuit-relay listens for the HOP | | | |
|
||||
| | libp2p-swarm <------------------------------------------------| circuit-relay | |
|
||||
| | | | message to handle incomming relay | | | |
|
||||
| | | | requests from other nodes | +------------------+ |
|
||||
| +---------------------------------+ | | |
|
||||
| ^ ^ ^ ^ ^ ^ | | +------------------+ |
|
||||
| | | | | | | | | | +-------------+ | |
|
||||
| | | | | | | | dialer uses libp2p-swarm to dial | | | | | |
|
||||
| | | | +----------------------------------------------------------------------> dialer | | |
|
||||
| | | transports | | to a circuit-relay node using the | | | | | |
|
||||
| | | | | | | HOP message | | +-------------+ | |
|
||||
| | | | | | | | | | |
|
||||
| v v | v v | | | | |
|
||||
|+------------------|----------------------------+| | | +-------------+ | |
|
||||
|| | | | | || | | | | | |
|
||||
||libp2p-tcp |libp2p-ws | .... |libp2p-circuit || listener handles STOP messages from| | | listener | | |
|
||||
|| | +--------------------------------------------------------------------------> | | |
|
||||
|| | | |plugs in just || circuit-relay nodes | | +-------------+ | |
|
||||
|| | | |as any other || | | | |
|
||||
|| | | |transport || | +------------------+ |
|
||||
|+-----------------------------------------------+| | |
|
||||
+-------------------------------------------------+ +--------------------------+
|
||||
```
|
136
src/circuit/circuit/hop.js
Normal file
136
src/circuit/circuit/hop.js
Normal file
@ -0,0 +1,136 @@
|
||||
'use strict'
|
||||
|
||||
const debug = require('debug')
|
||||
const PeerInfo = require('peer-info')
|
||||
const PeerId = require('peer-id')
|
||||
const { validateAddrs } = require('./utils')
|
||||
const StreamHandler = require('./stream-handler')
|
||||
const { CircuitRelay: CircuitPB } = require('../protocol')
|
||||
const pipe = require('it-pipe')
|
||||
const errCode = require('err-code')
|
||||
const { codes: Errors } = require('../../errors')
|
||||
|
||||
const { stop } = require('./stop')
|
||||
|
||||
const multicodec = require('./../multicodec')
|
||||
|
||||
const log = debug('libp2p:circuit:hop')
|
||||
log.error = debug('libp2p:circuit:hop:error')
|
||||
|
||||
module.exports.handleHop = async function handleHop ({
|
||||
connection,
|
||||
request,
|
||||
streamHandler,
|
||||
circuit
|
||||
}) {
|
||||
// Ensure hop is enabled
|
||||
if (!circuit._options.hop.enabled) {
|
||||
log('HOP request received but we are not acting as a relay')
|
||||
return streamHandler.end({
|
||||
type: CircuitPB.Type.STATUS,
|
||||
code: CircuitPB.Status.HOP_CANT_SPEAK_RELAY
|
||||
})
|
||||
}
|
||||
|
||||
// Validate the HOP request has the required input
|
||||
try {
|
||||
validateAddrs(request, streamHandler)
|
||||
} catch (err) {
|
||||
return log.error('invalid hop request via peer %s', connection.remotePeer.toB58String(), err)
|
||||
}
|
||||
|
||||
// Get the connection to the destination (stop) peer
|
||||
const destinationPeer = new PeerId(request.dstPeer.id)
|
||||
|
||||
const destinationConnection = circuit._registrar.getConnection(new PeerInfo(destinationPeer))
|
||||
if (!destinationConnection && !circuit._options.hop.active) {
|
||||
log('HOP request received but we are not connected to the destination peer')
|
||||
return streamHandler.end({
|
||||
type: CircuitPB.Type.STATUS,
|
||||
code: CircuitPB.Status.HOP_NO_CONN_TO_DST
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Handle being an active relay
|
||||
|
||||
// Handle the incoming HOP request by performing a STOP request
|
||||
const stopRequest = {
|
||||
type: CircuitPB.Type.STOP,
|
||||
dstPeer: request.dstPeer,
|
||||
srcPeer: request.srcPeer
|
||||
}
|
||||
|
||||
let destinationStream
|
||||
try {
|
||||
destinationStream = await stop({
|
||||
connection: destinationConnection,
|
||||
request: stopRequest,
|
||||
circuit
|
||||
})
|
||||
} catch (err) {
|
||||
return log.error(err)
|
||||
}
|
||||
|
||||
log('hop request from %s is valid', connection.remotePeer.toB58String())
|
||||
streamHandler.write({
|
||||
type: CircuitPB.Type.STATUS,
|
||||
code: CircuitPB.Status.SUCCESS
|
||||
})
|
||||
const sourceStream = streamHandler.rest()
|
||||
|
||||
// Short circuit the two streams to create the relayed connection
|
||||
return pipe(
|
||||
sourceStream,
|
||||
destinationStream,
|
||||
sourceStream
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a HOP request to a relay peer, to request a connection to another
|
||||
* peer. A new, virtual, connection will be created between the two via the relay.
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {Connection} options.connection Connection to the relay
|
||||
* @param {*} options.request
|
||||
* @param {Circuit} options.circuit
|
||||
* @returns {Promise<Connection>}
|
||||
*/
|
||||
module.exports.hop = async function hop ({
|
||||
connection,
|
||||
request
|
||||
}) {
|
||||
// Create a new stream to the relay
|
||||
const { stream } = await connection.newStream([multicodec.relay])
|
||||
// Send the HOP request
|
||||
const streamHandler = new StreamHandler({ stream })
|
||||
streamHandler.write(request)
|
||||
|
||||
const response = await streamHandler.read()
|
||||
|
||||
if (response.code === CircuitPB.Status.SUCCESS) {
|
||||
log('hop request was successful')
|
||||
return streamHandler.rest()
|
||||
}
|
||||
|
||||
log('hop request failed with code %d, closing stream', response.code)
|
||||
streamHandler.close()
|
||||
throw errCode(new Error(`HOP request failed with code ${response.code}`), Errors.ERR_HOP_REQUEST_FAILED)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an unencoded CAN_HOP response based on the Circuits configuration
|
||||
* @private
|
||||
*/
|
||||
module.exports.handleCanHop = function handleCanHop ({
|
||||
connection,
|
||||
streamHandler,
|
||||
circuit
|
||||
}) {
|
||||
const canHop = circuit._options.hop.enabled
|
||||
log('can hop (%s) request from %s', canHop, connection.remotePeer.toB58String())
|
||||
streamHandler.end({
|
||||
type: CircuitPB.Type.STATUS,
|
||||
code: canHop ? CircuitPB.Status.SUCCESS : CircuitPB.Status.HOP_CANT_SPEAK_RELAY
|
||||
})
|
||||
}
|
69
src/circuit/circuit/stop.js
Normal file
69
src/circuit/circuit/stop.js
Normal file
@ -0,0 +1,69 @@
|
||||
'use strict'
|
||||
|
||||
const { CircuitRelay: CircuitPB } = require('../protocol')
|
||||
const multicodec = require('../multicodec')
|
||||
const StreamHandler = require('./stream-handler')
|
||||
const { validateAddrs } = require('./utils')
|
||||
|
||||
const debug = require('debug')
|
||||
const log = debug('libp2p:circuit:stop')
|
||||
log.error = debug('libp2p:circuit:stop:error')
|
||||
|
||||
/**
|
||||
* Handles incoming STOP requests
|
||||
*
|
||||
* @private
|
||||
* @param {*} options
|
||||
* @param {Connection} options.connection
|
||||
* @param {*} options.request The CircuitRelay protobuf request (unencoded)
|
||||
* @param {StreamHandler} options.streamHandler
|
||||
* @returns {Promise<*>} Resolves a duplex iterable
|
||||
*/
|
||||
module.exports.handleStop = function handleStop ({
|
||||
connection,
|
||||
request,
|
||||
streamHandler
|
||||
}) {
|
||||
// Validate the STOP request has the required input
|
||||
try {
|
||||
validateAddrs(request, streamHandler)
|
||||
} catch (err) {
|
||||
return log.error('invalid stop request via peer %s', connection.remotePeer.toB58String(), err)
|
||||
}
|
||||
|
||||
// The request is valid
|
||||
log('stop request is valid')
|
||||
streamHandler.write({
|
||||
type: CircuitPB.Type.STATUS,
|
||||
code: CircuitPB.Status.SUCCESS
|
||||
})
|
||||
return streamHandler.rest()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a STOP request
|
||||
* @private
|
||||
* @param {*} options
|
||||
* @param {Connection} options.connection
|
||||
* @param {*} options.request The CircuitRelay protobuf request (unencoded)
|
||||
* @returns {Promise<*>} Resolves a duplex iterable
|
||||
*/
|
||||
module.exports.stop = async function stop ({
|
||||
connection,
|
||||
request
|
||||
}) {
|
||||
const { stream } = await connection.newStream([multicodec.relay])
|
||||
log('starting stop request to %s', connection.remotePeer.toB58String())
|
||||
const streamHandler = new StreamHandler({ stream })
|
||||
|
||||
streamHandler.write(request)
|
||||
const response = await streamHandler.read()
|
||||
|
||||
if (response.code === CircuitPB.Status.SUCCESS) {
|
||||
log('stop request to %s was successful', connection.remotePeer.toB58String())
|
||||
return streamHandler.rest()
|
||||
}
|
||||
|
||||
log('stop request failed with code %d', response.code)
|
||||
streamHandler.close()
|
||||
}
|
80
src/circuit/circuit/stream-handler.js
Normal file
80
src/circuit/circuit/stream-handler.js
Normal file
@ -0,0 +1,80 @@
|
||||
'use strict'
|
||||
|
||||
const lp = require('it-length-prefixed')
|
||||
const handshake = require('it-handshake')
|
||||
const { CircuitRelay: CircuitPB } = require('../protocol')
|
||||
|
||||
const debug = require('debug')
|
||||
const log = debug('libp2p:circuit:stream-handler')
|
||||
log.error = debug('libp2p:circuit:stream-handler:error')
|
||||
|
||||
class StreamHandler {
|
||||
/**
|
||||
* Create a stream handler for connection
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {*} options.stream - A duplex iterable
|
||||
* @param {Number} options.maxLength - max bytes length of message
|
||||
*/
|
||||
constructor ({ stream, maxLength = 4096 }) {
|
||||
this.stream = stream
|
||||
|
||||
this.shake = handshake(this.stream)
|
||||
this.decoder = lp.decode.fromReader(this.shake.reader, { maxDataLength: maxLength })
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and decode message
|
||||
* @async
|
||||
* @returns {void}
|
||||
*/
|
||||
async read () {
|
||||
const msg = await this.decoder.next()
|
||||
if (msg.value) {
|
||||
const value = CircuitPB.decode(msg.value.slice())
|
||||
log('read message type', value.type)
|
||||
return value
|
||||
}
|
||||
|
||||
log('read received no value, closing stream')
|
||||
// End the stream, we didn't get data
|
||||
this.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode and write array of buffers
|
||||
*
|
||||
* @param {*} msg An unencoded CircuitRelay protobuf message
|
||||
*/
|
||||
write (msg) {
|
||||
log('write message type %s', msg.type)
|
||||
this.shake.write(lp.encode.single(CircuitPB.encode(msg)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the handshake rest stream and invalidate handler
|
||||
*
|
||||
* @return {*} A duplex iterable
|
||||
*/
|
||||
rest () {
|
||||
this.shake.rest()
|
||||
return this.shake.stream
|
||||
}
|
||||
|
||||
end (msg) {
|
||||
this.write(msg)
|
||||
this.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the stream
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
close () {
|
||||
log('closing the stream')
|
||||
this.rest().sink([])
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StreamHandler
|
51
src/circuit/circuit/utils.js
Normal file
51
src/circuit/circuit/utils.js
Normal file
@ -0,0 +1,51 @@
|
||||
'use strict'
|
||||
|
||||
const multiaddr = require('multiaddr')
|
||||
const { CircuitRelay } = require('../protocol')
|
||||
|
||||
/**
|
||||
* Write a response
|
||||
*
|
||||
* @param {StreamHandler} streamHandler
|
||||
* @param {CircuitRelay.Status} status
|
||||
*/
|
||||
function writeResponse (streamHandler, status) {
|
||||
streamHandler.write({
|
||||
type: CircuitRelay.Type.STATUS,
|
||||
code: status
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate incomming HOP/STOP message
|
||||
*
|
||||
* @param {*} msg A CircuitRelay unencoded protobuf message
|
||||
* @param {StreamHandler} streamHandler
|
||||
*/
|
||||
function validateAddrs (msg, streamHandler) {
|
||||
try {
|
||||
msg.dstPeer.addrs.forEach((addr) => {
|
||||
return multiaddr(addr)
|
||||
})
|
||||
} catch (err) {
|
||||
writeResponse(streamHandler, msg.type === CircuitRelay.Type.HOP
|
||||
? CircuitRelay.Status.HOP_DST_MULTIADDR_INVALID
|
||||
: CircuitRelay.Status.STOP_DST_MULTIADDR_INVALID)
|
||||
throw err
|
||||
}
|
||||
|
||||
try {
|
||||
msg.srcPeer.addrs.forEach((addr) => {
|
||||
return multiaddr(addr)
|
||||
})
|
||||
} catch (err) {
|
||||
writeResponse(streamHandler, msg.type === CircuitRelay.Type.HOP
|
||||
? CircuitRelay.Status.HOP_SRC_MULTIADDR_INVALID
|
||||
: CircuitRelay.Status.STOP_SRC_MULTIADDR_INVALID)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateAddrs
|
||||
}
|
186
src/circuit/index.js
Normal file
186
src/circuit/index.js
Normal file
@ -0,0 +1,186 @@
|
||||
'use strict'
|
||||
|
||||
const mafmt = require('mafmt')
|
||||
const multiaddr = require('multiaddr')
|
||||
const PeerId = require('peer-id')
|
||||
const PeerInfo = require('peer-info')
|
||||
const withIs = require('class-is')
|
||||
const { CircuitRelay: CircuitPB } = require('./protocol')
|
||||
|
||||
const debug = require('debug')
|
||||
const log = debug('libp2p:circuit')
|
||||
log.error = debug('libp2p:circuit:error')
|
||||
|
||||
const { relay: multicodec } = require('./multicodec')
|
||||
const createListener = require('./listener')
|
||||
const { handleCanHop, handleHop, hop } = require('./circuit/hop')
|
||||
const { handleStop } = require('./circuit/stop')
|
||||
const StreamHandler = require('./circuit/stream-handler')
|
||||
const toConnection = require('./stream-to-conn')
|
||||
|
||||
class Circuit {
|
||||
/**
|
||||
* Creates an instance of Circuit.
|
||||
*
|
||||
* @constructor
|
||||
* @param {object} options
|
||||
* @param {Libp2p} options.libp2p
|
||||
* @param {Upgrader} options.upgrader
|
||||
*/
|
||||
constructor ({ libp2p, upgrader }) {
|
||||
this._dialer = libp2p.dialer
|
||||
this._registrar = libp2p.registrar
|
||||
this._upgrader = upgrader
|
||||
this._options = libp2p._config.relay
|
||||
this.peerInfo = libp2p.peerInfo
|
||||
this._registrar.handle(multicodec, this._onProtocol.bind(this))
|
||||
}
|
||||
|
||||
async _onProtocol ({ connection, stream, protocol }) {
|
||||
const streamHandler = new StreamHandler({ stream })
|
||||
const request = await streamHandler.read()
|
||||
const circuit = this
|
||||
let virtualConnection
|
||||
|
||||
switch (request.type) {
|
||||
case CircuitPB.Type.CAN_HOP: {
|
||||
log('received CAN_HOP request from %s', connection.remotePeer.toB58String())
|
||||
await handleCanHop({ circuit, connection, streamHandler })
|
||||
break
|
||||
}
|
||||
case CircuitPB.Type.HOP: {
|
||||
log('received HOP request from %s', connection.remotePeer.toB58String())
|
||||
virtualConnection = await handleHop({
|
||||
connection,
|
||||
request,
|
||||
streamHandler,
|
||||
circuit
|
||||
})
|
||||
break
|
||||
}
|
||||
case CircuitPB.Type.STOP: {
|
||||
log('received STOP request from %s', connection.remotePeer.toB58String())
|
||||
virtualConnection = await handleStop({
|
||||
connection,
|
||||
request,
|
||||
streamHandler,
|
||||
circuit
|
||||
})
|
||||
break
|
||||
}
|
||||
default: {
|
||||
log('Request of type %s not supported', request.type)
|
||||
}
|
||||
}
|
||||
|
||||
if (virtualConnection) {
|
||||
const remoteAddr = multiaddr(request.dstPeer.addrs[0])
|
||||
const localAddr = multiaddr(request.srcPeer.addrs[0])
|
||||
const maConn = toConnection({
|
||||
stream: virtualConnection,
|
||||
remoteAddr,
|
||||
localAddr
|
||||
})
|
||||
const type = CircuitPB.Type === CircuitPB.Type.HOP ? 'relay' : 'inbound'
|
||||
log('new %s connection %s', type, maConn.remoteAddr)
|
||||
|
||||
const conn = await this._upgrader.upgradeInbound(maConn)
|
||||
log('%s connection %s upgraded', type, maConn.remoteAddr)
|
||||
this.handler && this.handler(conn)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dial a peer over a relay
|
||||
*
|
||||
* @param {multiaddr} ma - the multiaddr of the peer to dial
|
||||
* @param {Object} options - dial options
|
||||
* @param {AbortSignal} [options.signal] - An optional abort signal
|
||||
* @returns {Connection} - the connection
|
||||
*/
|
||||
async dial (ma, options) {
|
||||
// Check the multiaddr to see if it contains a relay and a destination peer
|
||||
const addrs = ma.toString().split('/p2p-circuit')
|
||||
const relayAddr = multiaddr(addrs[0])
|
||||
const destinationAddr = multiaddr(addrs[addrs.length - 1])
|
||||
const relayPeer = PeerId.createFromCID(relayAddr.getPeerId())
|
||||
const destinationPeer = PeerId.createFromCID(destinationAddr.getPeerId())
|
||||
|
||||
let disconnectOnFailure = false
|
||||
let relayConnection = this._registrar.getConnection(new PeerInfo(relayPeer))
|
||||
if (!relayConnection) {
|
||||
relayConnection = await this._dialer.connectToPeer(relayAddr, options)
|
||||
disconnectOnFailure = true
|
||||
}
|
||||
|
||||
try {
|
||||
const virtualConnection = await hop({
|
||||
connection: relayConnection,
|
||||
circuit: this,
|
||||
request: {
|
||||
type: CircuitPB.Type.HOP,
|
||||
srcPeer: {
|
||||
id: this.peerInfo.id.toBytes(),
|
||||
addrs: this.peerInfo.multiaddrs.toArray().map(addr => addr.buffer)
|
||||
},
|
||||
dstPeer: {
|
||||
id: destinationPeer.toBytes(),
|
||||
addrs: [multiaddr(destinationAddr).buffer]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const localAddr = relayAddr.encapsulate(`/p2p-circuit/p2p/${this.peerInfo.id.toB58String()}`)
|
||||
const maConn = toConnection({
|
||||
stream: virtualConnection,
|
||||
remoteAddr: ma,
|
||||
localAddr
|
||||
})
|
||||
log('new outbound connection %s', maConn.remoteAddr)
|
||||
|
||||
return this._upgrader.upgradeOutbound(maConn)
|
||||
} catch (err) {
|
||||
log.error('Circuit relay dial failed', err)
|
||||
disconnectOnFailure && await relayConnection.close()
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a listener
|
||||
*
|
||||
* @param {any} options
|
||||
* @param {Function} handler
|
||||
* @return {listener}
|
||||
*/
|
||||
createListener (options, handler) {
|
||||
if (typeof options === 'function') {
|
||||
handler = options
|
||||
options = {}
|
||||
}
|
||||
|
||||
// Called on successful HOP and STOP requests
|
||||
this.handler = handler
|
||||
|
||||
return createListener(this, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter check for all Multiaddrs that this transport can dial on
|
||||
*
|
||||
* @param {Array<Multiaddr>} multiaddrs
|
||||
* @returns {Array<Multiaddr>}
|
||||
*/
|
||||
filter (multiaddrs) {
|
||||
multiaddrs = Array.isArray(multiaddrs) ? multiaddrs : [multiaddrs]
|
||||
|
||||
return multiaddrs.filter((ma) => {
|
||||
return mafmt.Circuit.matches(ma)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Circuit}
|
||||
*/
|
||||
module.exports = withIs(Circuit, { className: 'Circuit', symbolName: '@libp2p/js-libp2p-circuit/circuit' })
|
66
src/circuit/listener.js
Normal file
66
src/circuit/listener.js
Normal file
@ -0,0 +1,66 @@
|
||||
'use strict'
|
||||
|
||||
const EventEmitter = require('events')
|
||||
const multiaddr = require('multiaddr')
|
||||
|
||||
const debug = require('debug')
|
||||
const log = debug('libp2p:circuit:listener')
|
||||
log.err = debug('libp2p:circuit:error:listener')
|
||||
|
||||
/**
|
||||
* @param {*} circuit
|
||||
* @returns {Listener} a transport listener
|
||||
*/
|
||||
module.exports = (circuit) => {
|
||||
const listener = new EventEmitter()
|
||||
const listeningAddrs = new Map()
|
||||
|
||||
/**
|
||||
* Add swarm handler and listen for incoming connections
|
||||
*
|
||||
* @param {Multiaddr} addr
|
||||
* @return {void}
|
||||
*/
|
||||
listener.listen = async (addr) => {
|
||||
const [addrString] = String(addr).split('/p2p-circuit').slice(-1)
|
||||
|
||||
const relayConn = await circuit._dialer.connectToPeer(multiaddr(addrString))
|
||||
const relayedAddr = relayConn.remoteAddr.encapsulate('/p2p-circuit')
|
||||
|
||||
listeningAddrs.set(relayConn.remotePeer.toB58String(), relayedAddr)
|
||||
listener.emit('listening')
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Remove the peers from our topology
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
listener.close = () => {}
|
||||
|
||||
/**
|
||||
* Get fixed up multiaddrs
|
||||
*
|
||||
* NOTE: This method will grab the peers multiaddrs and expand them such that:
|
||||
*
|
||||
* a) If it's an existing /p2p-circuit address for a specific relay i.e.
|
||||
* `/ip4/0.0.0.0/tcp/0/ipfs/QmRelay/p2p-circuit` this method will expand the
|
||||
* address to `/ip4/0.0.0.0/tcp/0/ipfs/QmRelay/p2p-circuit/ipfs/QmPeer` where
|
||||
* `QmPeer` is this peers id
|
||||
* b) If it's not a /p2p-circuit address, it will encapsulate the address as a /p2p-circuit
|
||||
* addr, such when dialing over a relay with this address, it will create the circuit using
|
||||
* the encapsulated transport address. This is useful when for example, a peer should only
|
||||
* be dialed over TCP rather than any other transport
|
||||
*
|
||||
* @return {Multiaddr[]}
|
||||
*/
|
||||
listener.getAddrs = () => {
|
||||
const addrs = []
|
||||
for (const addr of listeningAddrs.values()) {
|
||||
addrs.push(addr)
|
||||
}
|
||||
return addrs
|
||||
}
|
||||
|
||||
return listener
|
||||
}
|
5
src/circuit/multicodec.js
Normal file
5
src/circuit/multicodec.js
Normal file
@ -0,0 +1,5 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = {
|
||||
relay: '/libp2p/circuit/relay/0.1.0'
|
||||
}
|
44
src/circuit/protocol/index.js
Normal file
44
src/circuit/protocol/index.js
Normal file
@ -0,0 +1,44 @@
|
||||
'use strict'
|
||||
const protobuf = require('protons')
|
||||
module.exports = protobuf(`
|
||||
message CircuitRelay {
|
||||
|
||||
enum Status {
|
||||
SUCCESS = 100;
|
||||
HOP_SRC_ADDR_TOO_LONG = 220;
|
||||
HOP_DST_ADDR_TOO_LONG = 221;
|
||||
HOP_SRC_MULTIADDR_INVALID = 250;
|
||||
HOP_DST_MULTIADDR_INVALID = 251;
|
||||
HOP_NO_CONN_TO_DST = 260;
|
||||
HOP_CANT_DIAL_DST = 261;
|
||||
HOP_CANT_OPEN_DST_STREAM = 262;
|
||||
HOP_CANT_SPEAK_RELAY = 270;
|
||||
HOP_CANT_RELAY_TO_SELF = 280;
|
||||
STOP_SRC_ADDR_TOO_LONG = 320;
|
||||
STOP_DST_ADDR_TOO_LONG = 321;
|
||||
STOP_SRC_MULTIADDR_INVALID = 350;
|
||||
STOP_DST_MULTIADDR_INVALID = 351;
|
||||
STOP_RELAY_REFUSED = 390;
|
||||
MALFORMED_MESSAGE = 400;
|
||||
}
|
||||
|
||||
enum Type { // RPC identifier, either HOP, STOP or STATUS
|
||||
HOP = 1;
|
||||
STOP = 2;
|
||||
STATUS = 3;
|
||||
CAN_HOP = 4;
|
||||
}
|
||||
|
||||
message Peer {
|
||||
required bytes id = 1; // peer id
|
||||
repeated bytes addrs = 2; // peer's known addresses
|
||||
}
|
||||
|
||||
optional Type type = 1; // Type of the message
|
||||
|
||||
optional Peer srcPeer = 2; // srcPeer and dstPeer are used when Type is HOP or STATUS
|
||||
optional Peer dstPeer = 3;
|
||||
|
||||
optional Status code = 4; // Status code, used when Type is STATUS
|
||||
}
|
||||
`)
|
49
src/circuit/stream-to-conn.js
Normal file
49
src/circuit/stream-to-conn.js
Normal file
@ -0,0 +1,49 @@
|
||||
'use strict'
|
||||
|
||||
const abortable = require('abortable-iterator')
|
||||
const log = require('debug')('libp2p:circuit:stream')
|
||||
|
||||
// Convert a duplex iterable into a MultiaddrConnection
|
||||
// https://github.com/libp2p/interface-transport#multiaddrconnection
|
||||
module.exports = ({ stream, remoteAddr, localAddr }, options = {}) => {
|
||||
const { sink, source } = stream
|
||||
const maConn = {
|
||||
async sink (source) {
|
||||
if (options.signal) {
|
||||
source = abortable(source, options.signal)
|
||||
}
|
||||
|
||||
try {
|
||||
await sink(source)
|
||||
} catch (err) {
|
||||
// If aborted we can safely ignore
|
||||
if (err.type !== 'aborted') {
|
||||
// If the source errored the socket will already have been destroyed by
|
||||
// toIterable.duplex(). If the socket errored it will already be
|
||||
// destroyed. There's nothing to do here except log the error & return.
|
||||
log(err)
|
||||
}
|
||||
}
|
||||
close()
|
||||
},
|
||||
|
||||
source: options.signal ? abortable(source, options.signal) : source,
|
||||
conn: stream,
|
||||
localAddr,
|
||||
remoteAddr,
|
||||
timeline: { open: Date.now() },
|
||||
|
||||
close () {
|
||||
sink([])
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
function close () {
|
||||
if (!maConn.timeline.close) {
|
||||
maConn.timeline.close = Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
return maConn
|
||||
}
|
131
src/config.js
131
src/config.js
@ -1,101 +1,48 @@
|
||||
'use strict'
|
||||
|
||||
const { struct, superstruct } = require('superstruct')
|
||||
const { optional, list } = struct
|
||||
const mergeOptions = require('merge-options')
|
||||
|
||||
// Define custom types
|
||||
const s = superstruct()
|
||||
const transport = s.union([
|
||||
s.interface({
|
||||
createListener: 'function',
|
||||
dial: 'function'
|
||||
}),
|
||||
'function'
|
||||
])
|
||||
const modulesSchema = s({
|
||||
connEncryption: optional(list([s('object|function')])),
|
||||
// this is hacky to simulate optional because interface doesnt work correctly with it
|
||||
// change to optional when fixed upstream
|
||||
connProtector: s.union(['undefined', s.interface({ protect: 'function' })]),
|
||||
contentRouting: optional(list(['object'])),
|
||||
dht: optional(s('null|function|object')),
|
||||
peerDiscovery: optional(list([s('object|function')])),
|
||||
peerRouting: optional(list(['object'])),
|
||||
streamMuxer: optional(list([s('object|function')])),
|
||||
transport: s.intersection([[transport], s.interface({
|
||||
length (v) {
|
||||
return v > 0 ? true : 'ERROR_EMPTY'
|
||||
}
|
||||
})])
|
||||
})
|
||||
|
||||
const configSchema = s({
|
||||
peerDiscovery: s('object', {
|
||||
autoDial: true
|
||||
}),
|
||||
relay: s({
|
||||
enabled: 'boolean',
|
||||
hop: optional(s({
|
||||
enabled: 'boolean',
|
||||
active: 'boolean'
|
||||
}, {
|
||||
// HOP defaults
|
||||
enabled: false,
|
||||
active: false
|
||||
}))
|
||||
}, {
|
||||
// Relay defaults
|
||||
enabled: true
|
||||
}),
|
||||
// DHT config
|
||||
dht: s('object?', {
|
||||
// DHT defaults
|
||||
enabled: false,
|
||||
kBucketSize: 20,
|
||||
randomWalk: {
|
||||
enabled: false, // disabled waiting for https://github.com/libp2p/js-libp2p-kad-dht/issues/86
|
||||
queriesPerPeriod: 1,
|
||||
interval: 300e3,
|
||||
timeout: 10e3
|
||||
}
|
||||
}),
|
||||
// Experimental config
|
||||
EXPERIMENTAL: s({
|
||||
pubsub: 'boolean'
|
||||
}, {
|
||||
// Experimental defaults
|
||||
pubsub: false
|
||||
})
|
||||
}, {})
|
||||
|
||||
const optionsSchema = s({
|
||||
switch: 'object?',
|
||||
connectionManager: s('object', {
|
||||
const DefaultConfig = {
|
||||
connectionManager: {
|
||||
minPeers: 25
|
||||
}),
|
||||
datastore: 'object?',
|
||||
peerInfo: 'object',
|
||||
peerBook: 'object?',
|
||||
modules: modulesSchema,
|
||||
config: configSchema
|
||||
})
|
||||
},
|
||||
metrics: {
|
||||
enabled: false
|
||||
},
|
||||
config: {
|
||||
dht: {
|
||||
enabled: false,
|
||||
kBucketSize: 20,
|
||||
randomWalk: {
|
||||
enabled: false, // disabled waiting for https://github.com/libp2p/js-libp2p-kad-dht/issues/86
|
||||
queriesPerPeriod: 1,
|
||||
interval: 300e3,
|
||||
timeout: 10e3
|
||||
}
|
||||
},
|
||||
peerDiscovery: {
|
||||
autoDial: true
|
||||
},
|
||||
pubsub: {
|
||||
enabled: true,
|
||||
emitSelf: true,
|
||||
signMessages: true,
|
||||
strictSigning: true
|
||||
},
|
||||
relay: {
|
||||
enabled: true,
|
||||
hop: {
|
||||
enabled: false,
|
||||
active: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.validate = (opts) => {
|
||||
const [error, options] = optionsSchema.validate(opts)
|
||||
opts = mergeOptions(DefaultConfig, opts)
|
||||
|
||||
// Improve errors throwed, reduce stack by throwing here and add reason to the message
|
||||
if (error) {
|
||||
throw new Error(`${error.message}${error.reason ? ' - ' + error.reason : ''}`)
|
||||
} else {
|
||||
// Throw when dht is enabled but no dht module provided
|
||||
if (options.config.dht.enabled) {
|
||||
s('function|object')(options.modules.dht)
|
||||
}
|
||||
}
|
||||
if (opts.modules.transport.length < 1) throw new Error("'options.modules.transport' must contain at least 1 transport")
|
||||
|
||||
if (options.config.peerDiscovery.autoDial === undefined) {
|
||||
options.config.peerDiscovery.autoDial = true
|
||||
}
|
||||
|
||||
return options
|
||||
return opts
|
||||
}
|
||||
|
192
src/connection-manager/index.js
Normal file
192
src/connection-manager/index.js
Normal file
@ -0,0 +1,192 @@
|
||||
'use strict'
|
||||
|
||||
const assert = require('assert')
|
||||
const mergeOptions = require('merge-options')
|
||||
const LatencyMonitor = require('latency-monitor').default
|
||||
const debug = require('debug')('libp2p:connection-manager')
|
||||
const retimer = require('retimer')
|
||||
|
||||
const defaultOptions = {
|
||||
maxConnections: Infinity,
|
||||
minConnections: 0,
|
||||
maxData: Infinity,
|
||||
maxSentData: Infinity,
|
||||
maxReceivedData: Infinity,
|
||||
maxEventLoopDelay: Infinity,
|
||||
pollInterval: 2000,
|
||||
movingAverageInterval: 60000,
|
||||
defaultPeerValue: 1
|
||||
}
|
||||
|
||||
class ConnectionManager {
|
||||
/**
|
||||
* @constructor
|
||||
* @param {Libp2p} libp2p
|
||||
* @param {object} options
|
||||
* @param {Number} options.maxConnections The maximum number of connections allowed. Default=Infinity
|
||||
* @param {Number} options.minConnections The minimum number of connections to avoid pruning. Default=0
|
||||
* @param {Number} options.maxData The max data (in and out), per average interval to allow. Default=Infinity
|
||||
* @param {Number} options.maxSentData The max outgoing data, per average interval to allow. Default=Infinity
|
||||
* @param {Number} options.maxReceivedData The max incoming data, per average interval to allow.. Default=Infinity
|
||||
* @param {Number} options.maxEventLoopDelay The upper limit the event loop can take to run. Default=Infinity
|
||||
* @param {Number} options.pollInterval How often, in milliseconds, metrics and latency should be checked. Default=2000
|
||||
* @param {Number} options.movingAverageInterval How often, in milliseconds, to compute averages. Default=60000
|
||||
* @param {Number} options.defaultPeerValue The value of the peer. Default=1
|
||||
*/
|
||||
constructor (libp2p, options) {
|
||||
this._libp2p = libp2p
|
||||
this._registrar = libp2p.registrar
|
||||
this._peerId = libp2p.peerInfo.id.toString()
|
||||
this._options = mergeOptions(defaultOptions, options)
|
||||
assert(
|
||||
this._options.maxConnections > this._options.minConnections,
|
||||
'Connection Manager maxConnections must be greater than minConnections'
|
||||
)
|
||||
|
||||
debug('options: %j', this._options)
|
||||
|
||||
this._metrics = libp2p.metrics
|
||||
|
||||
this._peerValues = new Map()
|
||||
this._connections = new Map()
|
||||
this._timer = null
|
||||
this._checkMetrics = this._checkMetrics.bind(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the Connection Manager. If Metrics are not enabled on libp2p
|
||||
* only event loop and connection limits will be monitored.
|
||||
*/
|
||||
start () {
|
||||
if (this._metrics) {
|
||||
this._timer = this._timer || retimer(this._checkMetrics, this._options.pollInterval)
|
||||
}
|
||||
|
||||
// latency monitor
|
||||
this._latencyMonitor = new LatencyMonitor({
|
||||
latencyCheckIntervalMs: this._options.pollInterval,
|
||||
dataEmitIntervalMs: this._options.pollInterval
|
||||
})
|
||||
this._onLatencyMeasure = this._onLatencyMeasure.bind(this)
|
||||
this._latencyMonitor.on('data', this._onLatencyMeasure)
|
||||
debug('started')
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the Connection Manager
|
||||
*/
|
||||
stop () {
|
||||
this._timer && this._timer.clear()
|
||||
this._latencyMonitor && this._latencyMonitor.removeListener('data', this._onLatencyMeasure)
|
||||
debug('stopped')
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of the given peer. Peers with lower values
|
||||
* will be disconnected first.
|
||||
* @param {PeerId} peerId
|
||||
* @param {number} value A number between 0 and 1
|
||||
*/
|
||||
setPeerValue (peerId, value) {
|
||||
if (value < 0 || value > 1) {
|
||||
throw new Error('value should be a number between 0 and 1')
|
||||
}
|
||||
if (peerId.toString) {
|
||||
peerId = peerId.toString()
|
||||
}
|
||||
this._peerValues.set(peerId, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the libp2p metrics to determine if any values have exceeded
|
||||
* the configured maximums.
|
||||
* @private
|
||||
*/
|
||||
_checkMetrics () {
|
||||
const movingAverages = this._metrics.global.movingAverages
|
||||
const received = movingAverages.dataReceived[this._options.movingAverageInterval].movingAverage()
|
||||
this._checkLimit('maxReceivedData', received)
|
||||
const sent = movingAverages.dataSent[this._options.movingAverageInterval].movingAverage()
|
||||
this._checkLimit('maxSentData', sent)
|
||||
const total = received + sent
|
||||
this._checkLimit('maxData', total)
|
||||
debug('metrics update', total)
|
||||
this._timer.reschedule(this._options.pollInterval)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks the incoming connection and check the connection limit
|
||||
* @param {Connection} connection
|
||||
*/
|
||||
onConnect (connection) {
|
||||
const peerId = connection.remotePeer.toString()
|
||||
this._connections.set(connection.id, connection)
|
||||
if (!this._peerValues.has(peerId)) {
|
||||
this._peerValues.set(peerId, this._options.defaultPeerValue)
|
||||
}
|
||||
this._checkLimit('maxConnections', this._connections.size)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the connection from tracking
|
||||
* @param {Connection} connection
|
||||
*/
|
||||
onDisconnect (connection) {
|
||||
this._connections.delete(connection.id)
|
||||
this._peerValues.delete(connection.remotePeer.toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* If the event loop is slow, maybe close a connection
|
||||
* @private
|
||||
* @param {*} summary The LatencyMonitor summary
|
||||
*/
|
||||
_onLatencyMeasure (summary) {
|
||||
this._checkLimit('maxEventLoopDelay', summary.avgMs)
|
||||
}
|
||||
|
||||
/**
|
||||
* If the `value` of `name` has exceeded its limit, maybe close a connection
|
||||
* @private
|
||||
* @param {string} name The name of the field to check limits for
|
||||
* @param {number} value The current value of the field
|
||||
*/
|
||||
_checkLimit (name, value) {
|
||||
const limit = this._options[name]
|
||||
debug('checking limit of %s. current value: %d of %d', name, value, limit)
|
||||
if (value > limit) {
|
||||
debug('%s: limit exceeded: %s, %d', this._peerId, name, value)
|
||||
this._maybeDisconnectOne()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If we have more connections than our maximum, close a connection
|
||||
* to the lowest valued peer.
|
||||
* @private
|
||||
*/
|
||||
_maybeDisconnectOne () {
|
||||
if (this._options.minConnections < this._connections.size) {
|
||||
const peerValues = Array.from(this._peerValues).sort(byPeerValue)
|
||||
debug('%s: sorted peer values: %j', this._peerId, peerValues)
|
||||
const disconnectPeer = peerValues[0]
|
||||
if (disconnectPeer) {
|
||||
const peerId = disconnectPeer[0]
|
||||
debug('%s: lowest value peer is %s', this._peerId, peerId)
|
||||
debug('%s: closing a connection to %j', this._peerId, peerId)
|
||||
for (const connection of this._connections.values()) {
|
||||
if (connection.remotePeer.toString() === peerId) {
|
||||
connection.close()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ConnectionManager
|
||||
|
||||
function byPeerValue (peerValueEntryA, peerValueEntryB) {
|
||||
return peerValueEntryA[1] - peerValueEntryB[1]
|
||||
}
|
23
src/constants.js
Normal file
23
src/constants.js
Normal file
@ -0,0 +1,23 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = {
|
||||
DENY_TTL: 5 * 60 * 1e3, // How long before an errored peer can be dialed again
|
||||
DENY_ATTEMPTS: 5, // Num of unsuccessful dials before a peer is permanently denied
|
||||
DIAL_TIMEOUT: 30e3, // How long in ms a dial attempt is allowed to take
|
||||
MAX_COLD_CALLS: 50, // How many dials w/o protocols that can be queued
|
||||
MAX_PARALLEL_DIALS: 100, // Maximum allowed concurrent dials
|
||||
MAX_PER_PEER_DIALS: 4, // Allowed parallel dials per DialRequest
|
||||
QUARTER_HOUR: 15 * 60e3,
|
||||
PRIORITY_HIGH: 10,
|
||||
PRIORITY_LOW: 20,
|
||||
METRICS: {
|
||||
computeThrottleMaxQueueSize: 1000,
|
||||
computeThrottleTimeout: 2000,
|
||||
movingAverageIntervals: [
|
||||
60 * 1000, // 1 minute
|
||||
5 * 60 * 1000, // 5 minutes
|
||||
15 * 60 * 1000 // 15 minutes
|
||||
],
|
||||
maxOldPeersRetention: 50
|
||||
}
|
||||
}
|
@ -1,15 +1,18 @@
|
||||
'use strict'
|
||||
|
||||
const tryEach = require('async/tryEach')
|
||||
const parallel = require('async/parallel')
|
||||
const errCode = require('err-code')
|
||||
const { messages, codes } = require('./errors')
|
||||
|
||||
const all = require('it-all')
|
||||
const pAny = require('p-any')
|
||||
|
||||
module.exports = (node) => {
|
||||
const routers = node._modules.contentRouting || []
|
||||
const dht = node._dht
|
||||
|
||||
// If we have the dht, make it first
|
||||
if (node._dht) {
|
||||
routers.unshift(node._dht)
|
||||
if (dht) {
|
||||
routers.unshift(dht)
|
||||
}
|
||||
|
||||
return {
|
||||
@ -18,48 +21,30 @@ module.exports = (node) => {
|
||||
* Once a content router succeeds, iteration will stop.
|
||||
*
|
||||
* @param {CID} key The CID key of the content to find
|
||||
* @param {object} options
|
||||
* @param {number} options.maxTimeout How long the query should run
|
||||
* @param {number} options.maxNumProviders - maximum number of providers to find
|
||||
* @param {function(Error, Result<Array>)} callback
|
||||
* @returns {void}
|
||||
* @param {object} [options]
|
||||
* @param {number} [options.timeout] How long the query should run
|
||||
* @param {number} [options.maxNumProviders] - maximum number of providers to find
|
||||
* @returns {AsyncIterable<PeerInfo>}
|
||||
*/
|
||||
findProviders: (key, options, callback) => {
|
||||
if (typeof options === 'function') {
|
||||
callback = options
|
||||
options = {}
|
||||
} else if (typeof options === 'number') { // This can be deprecated in a future release
|
||||
options = {
|
||||
maxTimeout: options
|
||||
}
|
||||
}
|
||||
|
||||
async * findProviders (key, options) {
|
||||
if (!routers.length) {
|
||||
return callback(errCode(new Error('No content routers available'), 'NO_ROUTERS_AVAILABLE'))
|
||||
throw errCode(new Error('No content routers available'), 'NO_ROUTERS_AVAILABLE')
|
||||
}
|
||||
|
||||
const tasks = routers.map((router) => {
|
||||
return (cb) => router.findProviders(key, options, (err, results) => {
|
||||
if (err) {
|
||||
return cb(err)
|
||||
}
|
||||
const result = await pAny(
|
||||
routers.map(async (router) => {
|
||||
const provs = await all(router.findProviders(key, options))
|
||||
|
||||
// If we don't have any results, we need to provide an error to keep trying
|
||||
if (!results || Object.keys(results).length === 0) {
|
||||
return cb(errCode(new Error('not found'), 'NOT_FOUND'), null)
|
||||
if (!provs || !provs.length) {
|
||||
throw errCode(new Error('not found'), 'NOT_FOUND')
|
||||
}
|
||||
|
||||
cb(null, results)
|
||||
return provs
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
tryEach(tasks, (err, results) => {
|
||||
if (err && err.code !== 'NOT_FOUND') {
|
||||
return callback(err)
|
||||
}
|
||||
results = results || []
|
||||
callback(null, results)
|
||||
})
|
||||
for (const pInfo of result) {
|
||||
yield pInfo
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@ -67,17 +52,62 @@ module.exports = (node) => {
|
||||
* a provider of the given key.
|
||||
*
|
||||
* @param {CID} key The CID key of the content to find
|
||||
* @param {function(Error)} callback
|
||||
* @returns {void}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
provide: (key, callback) => {
|
||||
async provide (key) { // eslint-disable-line require-await
|
||||
if (!routers.length) {
|
||||
return callback(errCode(new Error('No content routers available'), 'NO_ROUTERS_AVAILABLE'))
|
||||
throw errCode(new Error('No content routers available'), 'NO_ROUTERS_AVAILABLE')
|
||||
}
|
||||
|
||||
parallel(routers.map((router) => {
|
||||
return (cb) => router.provide(key, cb)
|
||||
}), callback)
|
||||
return Promise.all(routers.map((router) => router.provide(key)))
|
||||
},
|
||||
|
||||
/**
|
||||
* Store the given key/value pair in the DHT.
|
||||
* @param {Buffer} key
|
||||
* @param {Buffer} value
|
||||
* @param {Object} [options] - put options
|
||||
* @param {number} [options.minPeers] - minimum number of peers required to successfully put
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async put (key, value, options) { // eslint-disable-line require-await
|
||||
if (!node.isStarted() || !dht.isStarted) {
|
||||
throw errCode(new Error(messages.NOT_STARTED_YET), codes.DHT_NOT_STARTED)
|
||||
}
|
||||
|
||||
return dht.put(key, value, options)
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the value to the given key.
|
||||
* Times out after 1 minute by default.
|
||||
* @param {Buffer} key
|
||||
* @param {Object} [options] - get options
|
||||
* @param {number} [options.timeout] - optional timeout (default: 60000)
|
||||
* @returns {Promise<{from: PeerId, val: Buffer}>}
|
||||
*/
|
||||
async get (key, options) { // eslint-disable-line require-await
|
||||
if (!node.isStarted() || !dht.isStarted) {
|
||||
throw errCode(new Error(messages.NOT_STARTED_YET), codes.DHT_NOT_STARTED)
|
||||
}
|
||||
|
||||
return dht.get(key, options)
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the `n` values to the given key without sorting.
|
||||
* @param {Buffer} key
|
||||
* @param {number} nVals
|
||||
* @param {Object} [options] - get options
|
||||
* @param {number} [options.timeout] - optional timeout (default: 60000)
|
||||
* @returns {Promise<Array<{from: PeerId, val: Buffer}>>}
|
||||
*/
|
||||
async getMany (key, nVals, options) { // eslint-disable-line require-await
|
||||
if (!node.isStarted() || !dht.isStarted) {
|
||||
throw errCode(new Error(messages.NOT_STARTED_YET), codes.DHT_NOT_STARTED)
|
||||
}
|
||||
|
||||
return dht.getMany(key, nVals, options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
42
src/dht.js
42
src/dht.js
@ -1,42 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const nextTick = require('async/nextTick')
|
||||
const errCode = require('err-code')
|
||||
|
||||
const { messages, codes } = require('./errors')
|
||||
|
||||
module.exports = (node) => {
|
||||
return {
|
||||
put: (key, value, callback) => {
|
||||
if (!node._dht) {
|
||||
return nextTick(callback, errCode(new Error(messages.DHT_DISABLED), codes.DHT_DISABLED))
|
||||
}
|
||||
|
||||
node._dht.put(key, value, callback)
|
||||
},
|
||||
get: (key, options, callback) => {
|
||||
if (typeof options === 'function') {
|
||||
callback = options
|
||||
options = {}
|
||||
}
|
||||
|
||||
if (!node._dht) {
|
||||
return nextTick(callback, errCode(new Error(messages.DHT_DISABLED), codes.DHT_DISABLED))
|
||||
}
|
||||
|
||||
node._dht.get(key, options, callback)
|
||||
},
|
||||
getMany: (key, nVals, options, callback) => {
|
||||
if (typeof options === 'function') {
|
||||
callback = options
|
||||
options = {}
|
||||
}
|
||||
|
||||
if (!node._dht) {
|
||||
return nextTick(callback, errCode(new Error(messages.DHT_DISABLED), codes.DHT_DISABLED))
|
||||
}
|
||||
|
||||
node._dht.getMany(key, nVals, options, callback)
|
||||
}
|
||||
}
|
||||
}
|
80
src/dialer/dial-request.js
Normal file
80
src/dialer/dial-request.js
Normal file
@ -0,0 +1,80 @@
|
||||
'use strict'
|
||||
|
||||
const AbortController = require('abort-controller')
|
||||
const anySignal = require('any-signal')
|
||||
const debug = require('debug')
|
||||
const errCode = require('err-code')
|
||||
const log = debug('libp2p:dialer:request')
|
||||
log.error = debug('libp2p:dialer:request:error')
|
||||
const FIFO = require('p-fifo')
|
||||
const pAny = require('p-any')
|
||||
|
||||
class DialRequest {
|
||||
/**
|
||||
* Manages running the `dialAction` on multiple provided `addrs` in parallel
|
||||
* up to a maximum determined by the number of tokens returned
|
||||
* from `dialer.getTokens`. Once a DialRequest is created, it can be
|
||||
* started using `DialRequest.run(options)`. Once a single dial has succeeded,
|
||||
* all other dials in the request will be cancelled.
|
||||
* @param {object} options
|
||||
* @param {Multiaddr[]} options.addrs
|
||||
* @param {function(Multiaddr):Promise<Connection>} options.dialAction
|
||||
* @param {Dialer} options.dialer
|
||||
*/
|
||||
constructor ({
|
||||
addrs,
|
||||
dialAction,
|
||||
dialer
|
||||
}) {
|
||||
this.addrs = addrs
|
||||
this.dialer = dialer
|
||||
this.dialAction = dialAction
|
||||
}
|
||||
|
||||
/**
|
||||
* @async
|
||||
* @param {object} options
|
||||
* @param {AbortSignal} options.signal An AbortController signal
|
||||
* @returns {Connection}
|
||||
*/
|
||||
async run (options) {
|
||||
const tokens = this.dialer.getTokens(this.addrs.length)
|
||||
// If no tokens are available, throw
|
||||
if (tokens.length < 1) {
|
||||
throw errCode(new Error('No dial tokens available'), 'ERR_NO_DIAL_TOKENS')
|
||||
}
|
||||
|
||||
const tokenHolder = new FIFO()
|
||||
tokens.forEach(token => tokenHolder.push(token))
|
||||
const dialAbortControllers = this.addrs.map(() => new AbortController())
|
||||
let completedDials = 0
|
||||
|
||||
try {
|
||||
return await pAny(this.addrs.map(async (addr, i) => {
|
||||
const token = await tokenHolder.shift() // get token
|
||||
let conn
|
||||
try {
|
||||
const signal = dialAbortControllers[i].signal
|
||||
conn = await this.dialAction(addr, { ...options, signal: anySignal([signal, options.signal]) })
|
||||
// Remove the successful AbortController so it is not aborted
|
||||
dialAbortControllers.splice(i, 1)
|
||||
} finally {
|
||||
completedDials++
|
||||
// If we have more or equal dials remaining than tokens, recycle the token, otherwise release it
|
||||
if (this.addrs.length - completedDials >= tokens.length) {
|
||||
tokenHolder.push(token)
|
||||
} else {
|
||||
this.dialer.releaseToken(tokens.splice(tokens.indexOf(token), 1)[0])
|
||||
}
|
||||
}
|
||||
|
||||
return conn
|
||||
}))
|
||||
} finally {
|
||||
dialAbortControllers.map(c => c.abort()) // success/failure happened, abort everything else
|
||||
tokens.forEach(token => this.dialer.releaseToken(token)) // release tokens back to the dialer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.DialRequest = DialRequest
|
214
src/dialer/index.js
Normal file
214
src/dialer/index.js
Normal file
@ -0,0 +1,214 @@
|
||||
'use strict'
|
||||
|
||||
const multiaddr = require('multiaddr')
|
||||
const errCode = require('err-code')
|
||||
const TimeoutController = require('timeout-abort-controller')
|
||||
const anySignal = require('any-signal')
|
||||
const PeerId = require('peer-id')
|
||||
const PeerInfo = require('peer-info')
|
||||
const debug = require('debug')
|
||||
const log = debug('libp2p:dialer')
|
||||
log.error = debug('libp2p:dialer:error')
|
||||
const { DialRequest } = require('./dial-request')
|
||||
|
||||
const { codes } = require('../errors')
|
||||
const {
|
||||
DIAL_TIMEOUT,
|
||||
MAX_PARALLEL_DIALS,
|
||||
MAX_PER_PEER_DIALS
|
||||
} = require('../constants')
|
||||
|
||||
class Dialer {
|
||||
/**
|
||||
* @constructor
|
||||
* @param {object} options
|
||||
* @param {TransportManager} options.transportManager
|
||||
* @param {Peerstore} peerStore
|
||||
* @param {number} options.concurrency Number of max concurrent dials. Defaults to `MAX_PARALLEL_DIALS`
|
||||
* @param {number} options.timeout How long a dial attempt is allowed to take. Defaults to `DIAL_TIMEOUT`
|
||||
*/
|
||||
constructor ({
|
||||
transportManager,
|
||||
peerStore,
|
||||
concurrency = MAX_PARALLEL_DIALS,
|
||||
timeout = DIAL_TIMEOUT,
|
||||
perPeerLimit = MAX_PER_PEER_DIALS
|
||||
}) {
|
||||
this.transportManager = transportManager
|
||||
this.peerStore = peerStore
|
||||
this.concurrency = concurrency
|
||||
this.timeout = timeout
|
||||
this.perPeerLimit = perPeerLimit
|
||||
this.tokens = [...new Array(concurrency)].map((_, index) => index)
|
||||
this._pendingDials = new Map()
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears any pending dials
|
||||
*/
|
||||
destroy () {
|
||||
for (const dial of this._pendingDials.values()) {
|
||||
try {
|
||||
dial.controller.abort()
|
||||
} catch (err) {
|
||||
log.error(err)
|
||||
}
|
||||
}
|
||||
this._pendingDials.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to a given `PeerId` or `Multiaddr` by dialing all of its known addresses.
|
||||
* The dial to the first address that is successfully able to upgrade a connection
|
||||
* will be used.
|
||||
*
|
||||
* @param {PeerInfo|Multiaddr} peer The peer to dial
|
||||
* @param {object} [options]
|
||||
* @param {AbortSignal} [options.signal] An AbortController signal
|
||||
* @returns {Promise<Connection>}
|
||||
*/
|
||||
async connectToPeer (peer, options = {}) {
|
||||
const dialTarget = this._createDialTarget(peer)
|
||||
if (dialTarget.addrs.length === 0) {
|
||||
throw errCode(new Error('The dial request has no addresses'), 'ERR_NO_DIAL_MULTIADDRS')
|
||||
}
|
||||
const pendingDial = this._pendingDials.get(dialTarget.id) || this._createPendingDial(dialTarget, options)
|
||||
|
||||
try {
|
||||
const connection = await pendingDial.promise
|
||||
log('dial succeeded to %s', dialTarget.id)
|
||||
return connection
|
||||
} catch (err) {
|
||||
// Error is a timeout
|
||||
if (pendingDial.controller.signal.aborted) {
|
||||
err.code = codes.ERR_TIMEOUT
|
||||
}
|
||||
log.error(err)
|
||||
throw err
|
||||
} finally {
|
||||
pendingDial.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef DialTarget
|
||||
* @property {string} id
|
||||
* @property {Multiaddr[]} addrs
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a DialTarget. The DialTarget is used to create and track
|
||||
* the DialRequest to a given peer.
|
||||
* @private
|
||||
* @param {PeerInfo|Multiaddr} peer A PeerId or Multiaddr
|
||||
* @returns {DialTarget}
|
||||
*/
|
||||
_createDialTarget (peer) {
|
||||
const dialable = Dialer.getDialable(peer)
|
||||
if (multiaddr.isMultiaddr(dialable)) {
|
||||
return {
|
||||
id: dialable.toString(),
|
||||
addrs: [dialable]
|
||||
}
|
||||
}
|
||||
const addrs = this.peerStore.multiaddrsForPeer(dialable)
|
||||
return {
|
||||
id: dialable.id.toString(),
|
||||
addrs
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef PendingDial
|
||||
* @property {DialRequest} dialRequest
|
||||
* @property {TimeoutController} controller
|
||||
* @property {Promise} promise
|
||||
* @property {function():void} destroy
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a PendingDial that wraps the underlying DialRequest
|
||||
* @private
|
||||
* @param {DialTarget} dialTarget
|
||||
* @param {object} [options]
|
||||
* @param {AbortSignal} [options.signal] An AbortController signal
|
||||
* @returns {PendingDial}
|
||||
*/
|
||||
_createPendingDial (dialTarget, options) {
|
||||
const dialAction = (addr, options) => {
|
||||
if (options.signal.aborted) throw errCode(new Error('already aborted'), 'ERR_ALREADY_ABORTED')
|
||||
return this.transportManager.dial(addr, options)
|
||||
}
|
||||
|
||||
const dialRequest = new DialRequest({
|
||||
addrs: dialTarget.addrs,
|
||||
dialAction,
|
||||
dialer: this
|
||||
})
|
||||
|
||||
// Combine the timeout signal and options.signal, if provided
|
||||
const timeoutController = new TimeoutController(this.timeout)
|
||||
const signals = [timeoutController.signal]
|
||||
options.signal && signals.push(options.signal)
|
||||
const signal = anySignal(signals)
|
||||
|
||||
const pendingDial = {
|
||||
dialRequest,
|
||||
controller: timeoutController,
|
||||
promise: dialRequest.run({ ...options, signal }),
|
||||
destroy: () => {
|
||||
timeoutController.clear()
|
||||
this._pendingDials.delete(dialTarget.id)
|
||||
}
|
||||
}
|
||||
this._pendingDials.set(dialTarget.id, pendingDial)
|
||||
return pendingDial
|
||||
}
|
||||
|
||||
getTokens (num) {
|
||||
const total = Math.min(num, this.perPeerLimit, this.tokens.length)
|
||||
const tokens = this.tokens.splice(0, total)
|
||||
log('%d tokens request, returning %d, %d remaining', num, total, this.tokens.length)
|
||||
return tokens
|
||||
}
|
||||
|
||||
releaseToken (token) {
|
||||
// Guard against duplicate releases
|
||||
if (this.tokens.indexOf(token) > -1) return
|
||||
log('token %d released', token)
|
||||
this.tokens.push(token)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given `peer` into a `PeerInfo` or `Multiaddr`.
|
||||
* @static
|
||||
* @param {PeerInfo|PeerId|Multiaddr|string} peer
|
||||
* @returns {PeerInfo|Multiaddr}
|
||||
*/
|
||||
static getDialable (peer) {
|
||||
if (PeerInfo.isPeerInfo(peer)) return peer
|
||||
if (typeof peer === 'string') {
|
||||
peer = multiaddr(peer)
|
||||
}
|
||||
|
||||
let addr
|
||||
if (multiaddr.isMultiaddr(peer)) {
|
||||
addr = peer
|
||||
try {
|
||||
peer = PeerId.createFromCID(peer.getPeerId())
|
||||
} catch (err) {
|
||||
// Couldn't get the PeerId, just use the address
|
||||
return peer
|
||||
}
|
||||
}
|
||||
|
||||
if (PeerId.isPeerId(peer)) {
|
||||
peer = new PeerInfo(peer)
|
||||
}
|
||||
|
||||
addr && peer.multiaddrs.add(addr)
|
||||
return peer
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Dialer
|
@ -8,6 +8,21 @@ exports.messages = {
|
||||
exports.codes = {
|
||||
DHT_DISABLED: 'ERR_DHT_DISABLED',
|
||||
PUBSUB_NOT_STARTED: 'ERR_PUBSUB_NOT_STARTED',
|
||||
DHT_NOT_STARTED: 'ERR_DHT_NOT_STARTED',
|
||||
ERR_CONNECTION_ENDED: 'ERR_CONNECTION_ENDED',
|
||||
ERR_CONNECTION_FAILED: 'ERR_CONNECTION_FAILED',
|
||||
ERR_NODE_NOT_STARTED: 'ERR_NODE_NOT_STARTED',
|
||||
ERR_DISCOVERED_SELF: 'ERR_DISCOVERED_SELF'
|
||||
ERR_NO_VALID_ADDRESSES: 'ERR_NO_VALID_ADDRESSES',
|
||||
ERR_DISCOVERED_SELF: 'ERR_DISCOVERED_SELF',
|
||||
ERR_DUPLICATE_TRANSPORT: 'ERR_DUPLICATE_TRANSPORT',
|
||||
ERR_ENCRYPTION_FAILED: 'ERR_ENCRYPTION_FAILED',
|
||||
ERR_HOP_REQUEST_FAILED: 'ERR_HOP_REQUEST_FAILED',
|
||||
ERR_INVALID_KEY: 'ERR_INVALID_KEY',
|
||||
ERR_INVALID_MESSAGE: 'ERR_INVALID_MESSAGE',
|
||||
ERR_INVALID_PEER: 'ERR_INVALID_PEER',
|
||||
ERR_MUXER_UNAVAILABLE: 'ERR_MUXER_UNAVAILABLE',
|
||||
ERR_TIMEOUT: 'ERR_TIMEOUT',
|
||||
ERR_TRANSPORT_UNAVAILABLE: 'ERR_TRANSPORT_UNAVAILABLE',
|
||||
ERR_TRANSPORT_DIAL_FAILED: 'ERR_TRANSPORT_DIAL_FAILED',
|
||||
ERR_UNSUPPORTED_PROTOCOL: 'ERR_UNSUPPORTED_PROTOCOL'
|
||||
}
|
||||
|
@ -5,62 +5,69 @@ const PeerInfo = require('peer-info')
|
||||
const multiaddr = require('multiaddr')
|
||||
const errCode = require('err-code')
|
||||
|
||||
module.exports = (node) => {
|
||||
/*
|
||||
* Helper method to check the data type of peer and convert it to PeerInfo
|
||||
*/
|
||||
return function (peer, callback) {
|
||||
let p
|
||||
// PeerInfo
|
||||
if (PeerInfo.isPeerInfo(peer)) {
|
||||
p = peer
|
||||
// Multiaddr instance or Multiaddr String
|
||||
} else if (multiaddr.isMultiaddr(peer) || typeof peer === 'string') {
|
||||
if (typeof peer === 'string') {
|
||||
try {
|
||||
peer = multiaddr(peer)
|
||||
} catch (err) {
|
||||
return callback(
|
||||
errCode(err, 'ERR_INVALID_MULTIADDR')
|
||||
)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Converts the given `peer` to a `PeerInfo` instance.
|
||||
* The `PeerStore` will be checked for the resulting peer, and
|
||||
* the peer will be updated in the `PeerStore`.
|
||||
*
|
||||
* @param {PeerInfo|PeerId|Multiaddr|string} peer
|
||||
* @param {PeerStore} peerStore
|
||||
* @returns {PeerInfo}
|
||||
*/
|
||||
function getPeerInfo (peer, peerStore) {
|
||||
if (typeof peer === 'string') {
|
||||
peer = multiaddr(peer)
|
||||
}
|
||||
|
||||
const peerIdB58Str = peer.getPeerId()
|
||||
|
||||
if (!peerIdB58Str) {
|
||||
return callback(
|
||||
errCode(
|
||||
new Error('peer multiaddr instance or string must include peerId'),
|
||||
'ERR_INVALID_MULTIADDR'
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
p = node.peerBook.get(peerIdB58Str)
|
||||
} catch (err) {
|
||||
p = new PeerInfo(PeerId.createFromB58String(peerIdB58Str))
|
||||
}
|
||||
p.multiaddrs.add(peer)
|
||||
|
||||
// PeerId
|
||||
} else if (PeerId.isPeerId(peer)) {
|
||||
const peerIdB58Str = peer.toB58String()
|
||||
try {
|
||||
p = node.peerBook.get(peerIdB58Str)
|
||||
} catch (err) {
|
||||
return node.peerRouting.findPeer(peer, callback)
|
||||
}
|
||||
} else {
|
||||
return callback(
|
||||
errCode(
|
||||
new Error(`${p} is not a valid peer type`),
|
||||
'ERR_INVALID_PEER_TYPE'
|
||||
)
|
||||
let addr
|
||||
if (multiaddr.isMultiaddr(peer)) {
|
||||
addr = peer
|
||||
try {
|
||||
peer = PeerId.createFromB58String(peer.getPeerId())
|
||||
} catch (err) {
|
||||
throw errCode(
|
||||
new Error(`${peer} is not a valid peer type`),
|
||||
'ERR_INVALID_MULTIADDR'
|
||||
)
|
||||
}
|
||||
|
||||
callback(null, p)
|
||||
}
|
||||
|
||||
if (PeerId.isPeerId(peer)) {
|
||||
peer = new PeerInfo(peer)
|
||||
}
|
||||
|
||||
addr && peer.multiaddrs.add(addr)
|
||||
|
||||
return peerStore ? peerStore.put(peer) : peer
|
||||
}
|
||||
|
||||
/**
|
||||
* If `getPeerInfo` does not return a peer with multiaddrs,
|
||||
* the `libp2p` PeerRouter will be used to attempt to find the peer.
|
||||
*
|
||||
* @async
|
||||
* @param {PeerInfo|PeerId|Multiaddr|string} peer
|
||||
* @param {Libp2p} libp2p
|
||||
* @returns {Promise<PeerInfo>}
|
||||
*/
|
||||
function getPeerInfoRemote (peer, libp2p) {
|
||||
let peerInfo
|
||||
|
||||
try {
|
||||
peerInfo = getPeerInfo(peer, libp2p.peerStore)
|
||||
} catch (err) {
|
||||
throw errCode(err, 'ERR_INVALID_PEER_TYPE')
|
||||
}
|
||||
|
||||
// If we don't have an address for the peer, attempt to find it
|
||||
if (peerInfo.multiaddrs.size < 1) {
|
||||
return libp2p.peerRouting.findPeer(peerInfo.id)
|
||||
}
|
||||
|
||||
return peerInfo
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getPeerInfoRemote,
|
||||
getPeerInfo
|
||||
}
|
||||
|
13
src/identify/README.md
Normal file
13
src/identify/README.md
Normal file
@ -0,0 +1,13 @@
|
||||
# js-libp2p-identify
|
||||
|
||||
> libp2p Identify Protocol
|
||||
|
||||
**Note**: git history prior to merging into js-libp2p can be found in the original repository, https://github.com/libp2p/js-libp2p-identify.
|
||||
|
||||
## Description
|
||||
|
||||
Identify is a STUN protocol, used by libp2p in order to broadcast and learn about the `ip:port` pairs a specific peer is available through and to know when a new stream muxer is established, so a conn can be reused.
|
||||
|
||||
## How does it work
|
||||
|
||||
The spec for Identify and Identify Push is at [libp2p/specs](https://github.com/libp2p/specs/tree/master/identify).
|
6
src/identify/consts.js
Normal file
6
src/identify/consts.js
Normal file
@ -0,0 +1,6 @@
|
||||
'use strict'
|
||||
|
||||
module.exports.PROTOCOL_VERSION = 'ipfs/0.1.0'
|
||||
module.exports.AGENT_VERSION = 'js-libp2p/0.1.0'
|
||||
module.exports.MULTICODEC_IDENTIFY = '/ipfs/id/1.0.0'
|
||||
module.exports.MULTICODEC_IDENTIFY_PUSH = '/ipfs/id/push/1.0.0'
|
298
src/identify/index.js
Normal file
298
src/identify/index.js
Normal file
@ -0,0 +1,298 @@
|
||||
'use strict'
|
||||
|
||||
const debug = require('debug')
|
||||
const pb = require('it-protocol-buffers')
|
||||
const lp = require('it-length-prefixed')
|
||||
const pipe = require('it-pipe')
|
||||
const { collect, take } = require('streaming-iterables')
|
||||
|
||||
const PeerInfo = require('peer-info')
|
||||
const PeerId = require('peer-id')
|
||||
const multiaddr = require('multiaddr')
|
||||
const { toBuffer } = require('it-buffer')
|
||||
|
||||
const Message = require('./message')
|
||||
|
||||
const log = debug('libp2p:identify')
|
||||
log.error = debug('libp2p:identify:error')
|
||||
|
||||
const {
|
||||
MULTICODEC_IDENTIFY,
|
||||
MULTICODEC_IDENTIFY_PUSH,
|
||||
AGENT_VERSION,
|
||||
PROTOCOL_VERSION
|
||||
} = require('./consts')
|
||||
|
||||
const errCode = require('err-code')
|
||||
const { codes } = require('../errors')
|
||||
|
||||
class IdentifyService {
|
||||
/**
|
||||
* Replaces the multiaddrs on the given `peerInfo`,
|
||||
* with the provided `multiaddrs`
|
||||
* @param {PeerInfo} peerInfo
|
||||
* @param {Array<Multiaddr>|Array<Buffer>} multiaddrs
|
||||
*/
|
||||
static updatePeerAddresses (peerInfo, multiaddrs) {
|
||||
if (multiaddrs && multiaddrs.length > 0) {
|
||||
peerInfo.multiaddrs.clear()
|
||||
multiaddrs.forEach(ma => {
|
||||
try {
|
||||
peerInfo.multiaddrs.add(ma)
|
||||
} catch (err) {
|
||||
log.error('could not add multiaddr', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the protocols on the given `peerInfo`,
|
||||
* with the provided `protocols`
|
||||
* @static
|
||||
* @param {PeerInfo} peerInfo
|
||||
* @param {Array<string>} protocols
|
||||
*/
|
||||
static updatePeerProtocols (peerInfo, protocols) {
|
||||
if (protocols && protocols.length > 0) {
|
||||
peerInfo.protocols.clear()
|
||||
protocols.forEach(proto => peerInfo.protocols.add(proto))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the `addr` and converts it to a Multiaddr if possible
|
||||
* @param {Buffer|String} addr
|
||||
* @returns {Multiaddr|null}
|
||||
*/
|
||||
static getCleanMultiaddr (addr) {
|
||||
if (addr && addr.length > 0) {
|
||||
try {
|
||||
return multiaddr(addr)
|
||||
} catch (_) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {object} options
|
||||
* @param {Registrar} options.registrar
|
||||
* @param {Map<string, handler>} options.protocols A reference to the protocols we support
|
||||
* @param {PeerInfo} options.peerInfo The peer running the identify service
|
||||
*/
|
||||
constructor (options) {
|
||||
/**
|
||||
* @property {Registrar}
|
||||
*/
|
||||
this.registrar = options.registrar
|
||||
/**
|
||||
* @property {PeerInfo}
|
||||
*/
|
||||
this.peerInfo = options.peerInfo
|
||||
|
||||
this._protocols = options.protocols
|
||||
|
||||
this.handleMessage = this.handleMessage.bind(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an Identify Push update to the list of connections
|
||||
* @param {Array<Connection>} connections
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
push (connections) {
|
||||
const pushes = connections.map(async connection => {
|
||||
try {
|
||||
const { stream } = await connection.newStream(MULTICODEC_IDENTIFY_PUSH)
|
||||
|
||||
await pipe(
|
||||
[{
|
||||
listenAddrs: this.peerInfo.multiaddrs.toArray().map((ma) => ma.buffer),
|
||||
protocols: Array.from(this._protocols.keys())
|
||||
}],
|
||||
pb.encode(Message),
|
||||
stream
|
||||
)
|
||||
} catch (err) {
|
||||
// Just log errors
|
||||
log.error('could not push identify update to peer', err)
|
||||
}
|
||||
})
|
||||
|
||||
return Promise.all(pushes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls `push` for all peers in the `peerStore` that are connected
|
||||
* @param {PeerStore} peerStore
|
||||
*/
|
||||
pushToPeerStore (peerStore) {
|
||||
const connections = []
|
||||
let connection
|
||||
for (const peer of peerStore.peers.values()) {
|
||||
if (peer.protocols.has(MULTICODEC_IDENTIFY_PUSH) && (connection = this.registrar.getConnection(peer))) {
|
||||
connections.push(connection)
|
||||
}
|
||||
}
|
||||
|
||||
this.push(connections)
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the `Identify` message from peer associated with the given `connection`.
|
||||
* If the identified peer does not match the `PeerId` associated with the connection,
|
||||
* an error will be thrown.
|
||||
*
|
||||
* @async
|
||||
* @param {Connection} connection
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async identify (connection) {
|
||||
const { stream } = await connection.newStream(MULTICODEC_IDENTIFY)
|
||||
const [data] = await pipe(
|
||||
stream,
|
||||
lp.decode(),
|
||||
take(1),
|
||||
toBuffer,
|
||||
collect
|
||||
)
|
||||
|
||||
if (!data) {
|
||||
throw errCode(new Error('No data could be retrieved'), codes.ERR_CONNECTION_ENDED)
|
||||
}
|
||||
|
||||
let message
|
||||
try {
|
||||
message = Message.decode(data)
|
||||
} catch (err) {
|
||||
throw errCode(err, codes.ERR_INVALID_MESSAGE)
|
||||
}
|
||||
|
||||
let {
|
||||
publicKey,
|
||||
listenAddrs,
|
||||
protocols,
|
||||
observedAddr
|
||||
} = message
|
||||
|
||||
const id = await PeerId.createFromPubKey(publicKey)
|
||||
const peerInfo = new PeerInfo(id)
|
||||
if (connection.remotePeer.toString() !== id.toString()) {
|
||||
throw errCode(new Error('identified peer does not match the expected peer'), codes.ERR_INVALID_PEER)
|
||||
}
|
||||
|
||||
// Get the observedAddr if there is one
|
||||
observedAddr = IdentifyService.getCleanMultiaddr(observedAddr)
|
||||
|
||||
// Copy the listenAddrs and protocols
|
||||
IdentifyService.updatePeerAddresses(peerInfo, listenAddrs)
|
||||
IdentifyService.updatePeerProtocols(peerInfo, protocols)
|
||||
|
||||
this.registrar.peerStore.replace(peerInfo)
|
||||
// TODO: Track our observed address so that we can score it
|
||||
log('received observed address of %s', observedAddr)
|
||||
}
|
||||
|
||||
/**
|
||||
* A handler to register with Libp2p to process identify messages.
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {String} options.protocol
|
||||
* @param {*} options.stream
|
||||
* @param {Connection} options.connection
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
handleMessage ({ connection, stream, protocol }) {
|
||||
switch (protocol) {
|
||||
case MULTICODEC_IDENTIFY:
|
||||
return this._handleIdentify({ connection, stream })
|
||||
case MULTICODEC_IDENTIFY_PUSH:
|
||||
return this._handlePush({ connection, stream })
|
||||
default:
|
||||
log.error('cannot handle unknown protocol %s', protocol)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the `Identify` response to the requesting peer over the
|
||||
* given `connection`
|
||||
* @private
|
||||
* @param {object} options
|
||||
* @param {*} options.stream
|
||||
* @param {Connection} options.connection
|
||||
*/
|
||||
_handleIdentify ({ connection, stream }) {
|
||||
let publicKey = Buffer.alloc(0)
|
||||
if (this.peerInfo.id.pubKey) {
|
||||
publicKey = this.peerInfo.id.pubKey.bytes
|
||||
}
|
||||
|
||||
const message = Message.encode({
|
||||
protocolVersion: PROTOCOL_VERSION,
|
||||
agentVersion: AGENT_VERSION,
|
||||
publicKey,
|
||||
listenAddrs: this.peerInfo.multiaddrs.toArray().map((ma) => ma.buffer),
|
||||
observedAddr: connection.remoteAddr.buffer,
|
||||
protocols: Array.from(this._protocols.keys())
|
||||
})
|
||||
|
||||
pipe(
|
||||
[message],
|
||||
lp.encode(),
|
||||
stream
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the Identify Push message from the given `connection`
|
||||
* @private
|
||||
* @param {object} options
|
||||
* @param {*} options.stream
|
||||
* @param {Connection} options.connection
|
||||
*/
|
||||
async _handlePush ({ connection, stream }) {
|
||||
const [data] = await pipe(
|
||||
stream,
|
||||
lp.decode(),
|
||||
take(1),
|
||||
toBuffer,
|
||||
collect
|
||||
)
|
||||
|
||||
let message
|
||||
try {
|
||||
message = Message.decode(data)
|
||||
} catch (err) {
|
||||
return log.error('received invalid message', err)
|
||||
}
|
||||
|
||||
// Update the listen addresses
|
||||
const peerInfo = new PeerInfo(connection.remotePeer)
|
||||
|
||||
try {
|
||||
IdentifyService.updatePeerAddresses(peerInfo, message.listenAddrs)
|
||||
} catch (err) {
|
||||
return log.error('received invalid listen addrs', err)
|
||||
}
|
||||
|
||||
// Update the protocols
|
||||
IdentifyService.updatePeerProtocols(peerInfo, message.protocols)
|
||||
|
||||
// Update the peer in the PeerStore
|
||||
this.registrar.peerStore.replace(peerInfo)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.IdentifyService = IdentifyService
|
||||
/**
|
||||
* The protocols the IdentifyService supports
|
||||
* @property multicodecs
|
||||
*/
|
||||
module.exports.multicodecs = {
|
||||
IDENTIFY: MULTICODEC_IDENTIFY,
|
||||
IDENTIFY_PUSH: MULTICODEC_IDENTIFY_PUSH
|
||||
}
|
||||
module.exports.Message = Message
|
30
src/identify/message.js
Normal file
30
src/identify/message.js
Normal file
@ -0,0 +1,30 @@
|
||||
'use strict'
|
||||
|
||||
const protons = require('protons')
|
||||
const schema = `
|
||||
message Identify {
|
||||
// protocolVersion determines compatibility between peers
|
||||
optional string protocolVersion = 5; // e.g. ipfs/1.0.0
|
||||
|
||||
// agentVersion is like a UserAgent string in browsers, or client version in bittorrent
|
||||
// includes the client name and client.
|
||||
optional string agentVersion = 6; // e.g. go-ipfs/0.1.0
|
||||
|
||||
// publicKey is this node's public key (which also gives its node.ID)
|
||||
// - may not need to be sent, as secure channel implies it has been sent.
|
||||
// - then again, if we change / disable secure channel, may still want it.
|
||||
optional bytes publicKey = 1;
|
||||
|
||||
// listenAddrs are the multiaddrs the sender node listens for open connections on
|
||||
repeated bytes listenAddrs = 2;
|
||||
|
||||
// oservedAddr is the multiaddr of the remote endpoint that the sender node perceives
|
||||
// this is useful information to convey to the other side, as it helps the remote endpoint
|
||||
// determine whether its connection to the local peer goes through NAT.
|
||||
optional bytes observedAddr = 4;
|
||||
|
||||
repeated string protocols = 3;
|
||||
}
|
||||
`
|
||||
|
||||
module.exports = protons(schema).Identify
|
668
src/index.js
668
src/index.js
@ -1,47 +1,38 @@
|
||||
'use strict'
|
||||
|
||||
const FSM = require('fsm-event')
|
||||
const EventEmitter = require('events').EventEmitter
|
||||
const { EventEmitter } = require('events')
|
||||
const debug = require('debug')
|
||||
const log = debug('libp2p')
|
||||
log.error = debug('libp2p:error')
|
||||
const errCode = require('err-code')
|
||||
|
||||
const each = require('async/each')
|
||||
const series = require('async/series')
|
||||
const parallel = require('async/parallel')
|
||||
const nextTick = require('async/nextTick')
|
||||
|
||||
const PeerBook = require('peer-book')
|
||||
const PeerInfo = require('peer-info')
|
||||
const Switch = require('libp2p-switch')
|
||||
const Ping = require('libp2p-ping')
|
||||
const WebSockets = require('libp2p-websockets')
|
||||
const ConnectionManager = require('libp2p-connection-manager')
|
||||
|
||||
const { emitFirst } = require('./util')
|
||||
const peerRouting = require('./peer-routing')
|
||||
const contentRouting = require('./content-routing')
|
||||
const dht = require('./dht')
|
||||
const pubsub = require('./pubsub')
|
||||
const getPeerInfo = require('./get-peer-info')
|
||||
const validateConfig = require('./config').validate
|
||||
const { getPeerInfo } = require('./get-peer-info')
|
||||
const { validate: validateConfig } = require('./config')
|
||||
const { codes } = require('./errors')
|
||||
|
||||
const notStarted = (action, state) => {
|
||||
return errCode(
|
||||
new Error(`libp2p cannot ${action} when not started; state is ${state}`),
|
||||
codes.ERR_NODE_NOT_STARTED
|
||||
)
|
||||
}
|
||||
const ConnectionManager = require('./connection-manager')
|
||||
const Circuit = require('./circuit')
|
||||
const Dialer = require('./dialer')
|
||||
const Metrics = require('./metrics')
|
||||
const TransportManager = require('./transport-manager')
|
||||
const Upgrader = require('./upgrader')
|
||||
const PeerStore = require('./peer-store')
|
||||
const Registrar = require('./registrar')
|
||||
const ping = require('./ping')
|
||||
const {
|
||||
IdentifyService,
|
||||
multicodecs: IDENTIFY_PROTOCOLS
|
||||
} = require('./identify')
|
||||
|
||||
/**
|
||||
* @fires Libp2p#error Emitted when an error occurs
|
||||
* @fires Libp2p#peer:connect Emitted when a peer is connected to this node
|
||||
* @fires Libp2p#peer:disconnect Emitted when a peer disconnects from this node
|
||||
* @fires Libp2p#peer:discovery Emitted when a peer is discovered
|
||||
* @fires Libp2p#start Emitted when the node and its services has started
|
||||
* @fires Libp2p#stop Emitted when the node and its services has stopped
|
||||
*/
|
||||
class Libp2p extends EventEmitter {
|
||||
constructor (_options) {
|
||||
@ -52,140 +43,129 @@ class Libp2p extends EventEmitter {
|
||||
|
||||
this.datastore = this._options.datastore
|
||||
this.peerInfo = this._options.peerInfo
|
||||
this.peerBook = this._options.peerBook || new PeerBook()
|
||||
this.peerStore = new PeerStore()
|
||||
|
||||
this._modules = this._options.modules
|
||||
this._config = this._options.config
|
||||
this._transport = [] // Transport instances/references
|
||||
this._discovery = [] // Discovery service instances/references
|
||||
this._discovery = new Map() // Discovery service instances/references
|
||||
|
||||
// create the switch, and listen for errors
|
||||
this._switch = new Switch(this.peerInfo, this.peerBook, this._options.switch)
|
||||
this._switch.on('error', (...args) => this.emit('error', ...args))
|
||||
|
||||
this.stats = this._switch.stats
|
||||
this.connectionManager = new ConnectionManager(this, this._options.connectionManager)
|
||||
|
||||
// Attach stream multiplexers
|
||||
if (this._modules.streamMuxer) {
|
||||
let muxers = this._modules.streamMuxer
|
||||
muxers.forEach((muxer) => this._switch.connection.addStreamMuxer(muxer))
|
||||
|
||||
// If muxer exists
|
||||
// we can use Identify
|
||||
this._switch.connection.reuse()
|
||||
// we can use Relay for listening/dialing
|
||||
this._switch.connection.enableCircuitRelay(this._config.relay)
|
||||
|
||||
// Received incomming dial and muxer upgrade happened,
|
||||
// reuse this muxed connection
|
||||
this._switch.on('peer-mux-established', (peerInfo) => {
|
||||
this.emit('peer:connect', peerInfo)
|
||||
})
|
||||
|
||||
this._switch.on('peer-mux-closed', (peerInfo) => {
|
||||
this.emit('peer:disconnect', peerInfo)
|
||||
})
|
||||
if (this._options.metrics.enabled) {
|
||||
this.metrics = new Metrics(this._options.metrics)
|
||||
}
|
||||
|
||||
// Events for anytime connections are created/removed
|
||||
this._switch.on('connection:start', (peerInfo) => {
|
||||
this.emit('connection:start', peerInfo)
|
||||
// Setup the Upgrader
|
||||
this.upgrader = new Upgrader({
|
||||
localPeer: this.peerInfo.id,
|
||||
metrics: this.metrics,
|
||||
onConnection: (connection) => {
|
||||
const peerInfo = this.peerStore.put(new PeerInfo(connection.remotePeer), { silent: true })
|
||||
this.registrar.onConnect(peerInfo, connection)
|
||||
this.connectionManager.onConnect(connection)
|
||||
this.emit('peer:connect', peerInfo)
|
||||
|
||||
// Run identify for every connection
|
||||
if (this.identifyService) {
|
||||
this.identifyService.identify(connection, connection.remotePeer)
|
||||
.catch(log.error)
|
||||
}
|
||||
},
|
||||
onConnectionEnd: (connection) => {
|
||||
const peerInfo = Dialer.getDialable(connection.remotePeer)
|
||||
this.registrar.onDisconnect(peerInfo, connection)
|
||||
this.connectionManager.onDisconnect(connection)
|
||||
|
||||
// If there are no connections to the peer, disconnect
|
||||
if (!this.registrar.getConnection(peerInfo)) {
|
||||
this.emit('peer:disconnect', peerInfo)
|
||||
this.metrics && this.metrics.onPeerDisconnected(peerInfo.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
this._switch.on('connection:end', (peerInfo) => {
|
||||
this.emit('connection:end', peerInfo)
|
||||
|
||||
// Create the Registrar
|
||||
this.registrar = new Registrar({ peerStore: this.peerStore })
|
||||
this.handle = this.handle.bind(this)
|
||||
this.registrar.handle = this.handle
|
||||
|
||||
// Create the Connection Manager
|
||||
this.connectionManager = new ConnectionManager(this, this._options.connectionManager)
|
||||
|
||||
// Setup the transport manager
|
||||
this.transportManager = new TransportManager({
|
||||
libp2p: this,
|
||||
upgrader: this.upgrader
|
||||
})
|
||||
|
||||
// Attach crypto channels
|
||||
if (this._modules.connEncryption) {
|
||||
let cryptos = this._modules.connEncryption
|
||||
const cryptos = this._modules.connEncryption
|
||||
cryptos.forEach((crypto) => {
|
||||
this._switch.connection.crypto(crypto.tag, crypto.encrypt)
|
||||
this.upgrader.cryptos.set(crypto.protocol, crypto)
|
||||
})
|
||||
}
|
||||
|
||||
this.dialer = new Dialer({
|
||||
transportManager: this.transportManager,
|
||||
peerStore: this.peerStore
|
||||
})
|
||||
|
||||
this._modules.transport.forEach((Transport) => {
|
||||
this.transportManager.add(Transport.prototype[Symbol.toStringTag], Transport)
|
||||
})
|
||||
// TODO: enable relay if enabled
|
||||
this.transportManager.add(Circuit.prototype[Symbol.toStringTag], Circuit)
|
||||
|
||||
// Attach stream multiplexers
|
||||
if (this._modules.streamMuxer) {
|
||||
const muxers = this._modules.streamMuxer
|
||||
muxers.forEach((muxer) => {
|
||||
this.upgrader.muxers.set(muxer.multicodec, muxer)
|
||||
})
|
||||
|
||||
// Add the identify service since we can multiplex
|
||||
this.identifyService = new IdentifyService({
|
||||
registrar: this.registrar,
|
||||
peerInfo: this.peerInfo,
|
||||
protocols: this.upgrader.protocols
|
||||
})
|
||||
this.handle(Object.values(IDENTIFY_PROTOCOLS), this.identifyService.handleMessage)
|
||||
}
|
||||
|
||||
// Attach private network protector
|
||||
if (this._modules.connProtector) {
|
||||
this._switch.protector = this._modules.connProtector
|
||||
this.upgrader.protector = this._modules.connProtector
|
||||
} else if (process.env.LIBP2P_FORCE_PNET) {
|
||||
throw new Error('Private network is enforced, but no protector was provided')
|
||||
}
|
||||
|
||||
// dht provided components (peerRouting, contentRouting, dht)
|
||||
if (this._config.dht.enabled) {
|
||||
if (this._modules.dht) {
|
||||
const DHT = this._modules.dht
|
||||
|
||||
this._dht = new DHT(this._switch, {
|
||||
this._dht = new DHT({
|
||||
dialer: this.dialer,
|
||||
peerInfo: this.peerInfo,
|
||||
peerStore: this.peerStore,
|
||||
registrar: this.registrar,
|
||||
datastore: this.datastore,
|
||||
...this._config.dht
|
||||
})
|
||||
}
|
||||
|
||||
// enable/disable pubsub
|
||||
if (this._config.EXPERIMENTAL.pubsub) {
|
||||
this.pubsub = pubsub(this)
|
||||
// start pubsub
|
||||
if (this._modules.pubsub) {
|
||||
this.pubsub = pubsub(this, this._modules.pubsub, this._config.pubsub)
|
||||
}
|
||||
|
||||
// Attach remaining APIs
|
||||
// peer and content routing will automatically get modules from _modules and _dht
|
||||
this.peerRouting = peerRouting(this)
|
||||
this.contentRouting = contentRouting(this)
|
||||
this.dht = dht(this)
|
||||
|
||||
this._getPeerInfo = getPeerInfo(this)
|
||||
|
||||
// Mount default protocols
|
||||
Ping.mount(this._switch)
|
||||
ping.mount(this)
|
||||
|
||||
this.state = new FSM('STOPPED', {
|
||||
STOPPED: {
|
||||
start: 'STARTING',
|
||||
stop: 'STOPPED'
|
||||
},
|
||||
STARTING: {
|
||||
done: 'STARTED',
|
||||
abort: 'STOPPED',
|
||||
stop: 'STOPPING'
|
||||
},
|
||||
STARTED: {
|
||||
stop: 'STOPPING',
|
||||
start: 'STARTED'
|
||||
},
|
||||
STOPPING: {
|
||||
stop: 'STOPPING',
|
||||
done: 'STOPPED'
|
||||
}
|
||||
})
|
||||
this.state.on('STARTING', () => {
|
||||
log('libp2p is starting')
|
||||
this._onStarting()
|
||||
})
|
||||
this.state.on('STOPPING', () => {
|
||||
log('libp2p is stopping')
|
||||
this._onStopping()
|
||||
})
|
||||
this.state.on('STARTED', () => {
|
||||
log('libp2p has started')
|
||||
this.emit('start')
|
||||
})
|
||||
this.state.on('STOPPED', () => {
|
||||
log('libp2p has stopped')
|
||||
this.emit('stop')
|
||||
})
|
||||
this.state.on('error', (err) => {
|
||||
log.error(err)
|
||||
this.emit('error', err)
|
||||
})
|
||||
|
||||
// Once we start, emit and dial any peers we may have already discovered
|
||||
this.state.on('STARTED', () => {
|
||||
this.peerBook.getAllArray().forEach((peerInfo) => {
|
||||
this.emit('peer:discovery', peerInfo)
|
||||
this._maybeConnect(peerInfo)
|
||||
})
|
||||
})
|
||||
|
||||
this._peerDiscovered = this._peerDiscovered.bind(this)
|
||||
this._onDiscoveryPeer = this._onDiscoveryPeer.bind(this)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -204,291 +184,229 @@ class Libp2p extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the libp2p node and all sub services
|
||||
* Starts the libp2p node and all its subsystems
|
||||
*
|
||||
* @param {function(Error)} callback
|
||||
* @returns {void}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
start (callback = () => {}) {
|
||||
emitFirst(this, ['error', 'start'], callback)
|
||||
this.state('start')
|
||||
async start () {
|
||||
log('libp2p is starting')
|
||||
try {
|
||||
await this._onStarting()
|
||||
await this._onDidStart()
|
||||
log('libp2p has started')
|
||||
} catch (err) {
|
||||
this.emit('error', err)
|
||||
log.error('An error occurred starting libp2p', err)
|
||||
await this.stop()
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the libp2p node by closing its listeners and open connections
|
||||
*
|
||||
* @param {function(Error)} callback
|
||||
* @async
|
||||
* @returns {void}
|
||||
*/
|
||||
stop (callback = () => {}) {
|
||||
emitFirst(this, ['error', 'stop'], callback)
|
||||
this.state('stop')
|
||||
async stop () {
|
||||
log('libp2p is stopping')
|
||||
|
||||
try {
|
||||
this.connectionManager.stop()
|
||||
await Promise.all([
|
||||
this.pubsub && this.pubsub.stop(),
|
||||
this._dht && this._dht.stop(),
|
||||
this.metrics && this.metrics.stop()
|
||||
])
|
||||
|
||||
await this.transportManager.close()
|
||||
await this.registrar.close()
|
||||
|
||||
ping.unmount(this)
|
||||
this.dialer.destroy()
|
||||
} catch (err) {
|
||||
if (err) {
|
||||
log.error(err)
|
||||
this.emit('error', err)
|
||||
}
|
||||
}
|
||||
this._isStarted = false
|
||||
log('libp2p has stopped')
|
||||
}
|
||||
|
||||
isStarted () {
|
||||
return this.state ? this.state._state === 'STARTED' : false
|
||||
return this._isStarted
|
||||
}
|
||||
|
||||
/**
|
||||
* Dials to the provided peer. If successful, the `PeerInfo` of the
|
||||
* peer will be added to the nodes `PeerBook`
|
||||
* peer will be added to the nodes `peerStore`
|
||||
*
|
||||
* @param {PeerInfo|PeerId|Multiaddr|string} peer The peer to dial
|
||||
* @param {function(Error)} callback
|
||||
* @returns {void}
|
||||
* @param {object} options
|
||||
* @param {AbortSignal} [options.signal]
|
||||
* @returns {Promise<Connection>}
|
||||
*/
|
||||
dial (peer, callback) {
|
||||
this.dialProtocol(peer, null, callback)
|
||||
dial (peer, options) {
|
||||
return this.dialProtocol(peer, null, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dials to the provided peer and handshakes with the given protocol.
|
||||
* If successful, the `PeerInfo` of the peer will be added to the nodes `PeerBook`,
|
||||
* If successful, the `PeerInfo` of the peer will be added to the nodes `peerStore`,
|
||||
* and the `Connection` will be sent in the callback
|
||||
*
|
||||
* @async
|
||||
* @param {PeerInfo|PeerId|Multiaddr|string} peer The peer to dial
|
||||
* @param {string} protocol
|
||||
* @param {function(Error, Connection)} callback
|
||||
* @returns {void}
|
||||
* @param {string[]|string} protocols
|
||||
* @param {object} options
|
||||
* @param {AbortSignal} [options.signal]
|
||||
* @returns {Promise<Connection|*>}
|
||||
*/
|
||||
dialProtocol (peer, protocol, callback) {
|
||||
if (!this.isStarted()) {
|
||||
return callback(notStarted('dial', this.state._state))
|
||||
async dialProtocol (peer, protocols, options) {
|
||||
const dialable = Dialer.getDialable(peer)
|
||||
let connection
|
||||
if (PeerInfo.isPeerInfo(dialable)) {
|
||||
this.peerStore.put(dialable, { silent: true })
|
||||
connection = this.registrar.getConnection(dialable)
|
||||
}
|
||||
|
||||
if (typeof protocol === 'function') {
|
||||
callback = protocol
|
||||
protocol = undefined
|
||||
if (!connection) {
|
||||
connection = await this.dialer.connectToPeer(dialable, options)
|
||||
}
|
||||
|
||||
this._getPeerInfo(peer, (err, peerInfo) => {
|
||||
if (err) { return callback(err) }
|
||||
// If a protocol was provided, create a new stream
|
||||
if (protocols) {
|
||||
return connection.newStream(protocols)
|
||||
}
|
||||
|
||||
this._switch.dial(peerInfo, protocol, callback)
|
||||
})
|
||||
return connection
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to `dial` and `dialProtocol`, but the callback will contain a
|
||||
* Connection State Machine.
|
||||
* Disconnects all connections to the given `peer`
|
||||
*
|
||||
* @param {PeerInfo|PeerId|Multiaddr|string} peer The peer to dial
|
||||
* @param {string} protocol
|
||||
* @param {function(Error, ConnectionFSM)} callback
|
||||
* @returns {void}
|
||||
* @param {PeerId} peer The PeerId to close connections to
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
dialFSM (peer, protocol, callback) {
|
||||
if (!this.isStarted()) {
|
||||
return callback(notStarted('dial', this.state._state))
|
||||
}
|
||||
hangUp (peer) {
|
||||
return Promise.all(
|
||||
this.registrar.connections.get(peer.toString()).map(connection => {
|
||||
return connection.close()
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof protocol === 'function') {
|
||||
callback = protocol
|
||||
protocol = undefined
|
||||
}
|
||||
/**
|
||||
* Pings the given peer
|
||||
* @param {PeerInfo|PeerId|Multiaddr|string} peer The peer to ping
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
async ping (peer) {
|
||||
const peerInfo = await getPeerInfo(peer, this.peerStore)
|
||||
|
||||
this._getPeerInfo(peer, (err, peerInfo) => {
|
||||
if (err) { return callback(err) }
|
||||
return ping(this, peerInfo)
|
||||
}
|
||||
|
||||
this._switch.dialFSM(peerInfo, protocol, callback)
|
||||
/**
|
||||
* Registers the `handler` for each protocol
|
||||
* @param {string[]|string} protocols
|
||||
* @param {function({ connection:*, stream:*, protocol:string })} handler
|
||||
*/
|
||||
handle (protocols, handler) {
|
||||
protocols = Array.isArray(protocols) ? protocols : [protocols]
|
||||
protocols.forEach(protocol => {
|
||||
this.upgrader.protocols.set(protocol, handler)
|
||||
})
|
||||
}
|
||||
|
||||
hangUp (peer, callback) {
|
||||
this._getPeerInfo(peer, (err, peerInfo) => {
|
||||
if (err) { return callback(err) }
|
||||
|
||||
this._switch.hangUp(peerInfo, callback)
|
||||
})
|
||||
}
|
||||
|
||||
ping (peer, callback) {
|
||||
if (!this.isStarted()) {
|
||||
return callback(notStarted('ping', this.state._state))
|
||||
// Only push if libp2p is running
|
||||
if (this.isStarted() && this.identifyService) {
|
||||
this.identifyService.pushToPeerStore(this.peerStore)
|
||||
}
|
||||
}
|
||||
|
||||
this._getPeerInfo(peer, (err, peerInfo) => {
|
||||
if (err) { return callback(err) }
|
||||
|
||||
callback(null, new Ping(this._switch, peerInfo))
|
||||
/**
|
||||
* Removes the handler for each protocol. The protocol
|
||||
* will no longer be supported on streams.
|
||||
* @param {string[]|string} protocols
|
||||
*/
|
||||
unhandle (protocols) {
|
||||
protocols = Array.isArray(protocols) ? protocols : [protocols]
|
||||
protocols.forEach(protocol => {
|
||||
this.upgrader.protocols.delete(protocol)
|
||||
})
|
||||
}
|
||||
|
||||
handle (protocol, handlerFunc, matchFunc) {
|
||||
this._switch.handle(protocol, handlerFunc, matchFunc)
|
||||
}
|
||||
|
||||
unhandle (protocol) {
|
||||
this._switch.unhandle(protocol)
|
||||
}
|
||||
|
||||
_onStarting () {
|
||||
if (!this._modules.transport) {
|
||||
this.emit('error', new Error('no transports were present'))
|
||||
return this.state('abort')
|
||||
// Only push if libp2p is running
|
||||
if (this.isStarted() && this.identifyService) {
|
||||
this.identifyService.pushToPeerStore(this.peerStore)
|
||||
}
|
||||
}
|
||||
|
||||
let ws
|
||||
|
||||
// so that we can have webrtc-star addrs without adding manually the id
|
||||
const maOld = []
|
||||
const maNew = []
|
||||
this.peerInfo.multiaddrs.toArray().forEach((ma) => {
|
||||
if (!ma.getPeerId()) {
|
||||
maOld.push(ma)
|
||||
maNew.push(ma.encapsulate('/p2p/' + this.peerInfo.id.toB58String()))
|
||||
}
|
||||
})
|
||||
this.peerInfo.multiaddrs.replace(maOld, maNew)
|
||||
|
||||
async _onStarting () {
|
||||
// Listen on the addresses supplied in the peerInfo
|
||||
const multiaddrs = this.peerInfo.multiaddrs.toArray()
|
||||
|
||||
this._modules.transport.forEach((Transport) => {
|
||||
let t
|
||||
await this.transportManager.listen(multiaddrs)
|
||||
|
||||
if (typeof Transport === 'function') {
|
||||
t = new Transport({ libp2p: this })
|
||||
} else {
|
||||
t = Transport
|
||||
}
|
||||
// The addresses may change once the listener starts
|
||||
// eg /ip4/0.0.0.0/tcp/0 => /ip4/192.168.1.0/tcp/58751
|
||||
this.peerInfo.multiaddrs.clear()
|
||||
for (const ma of this.transportManager.getAddrs()) {
|
||||
this.peerInfo.multiaddrs.add(ma)
|
||||
}
|
||||
|
||||
if (t.filter(multiaddrs).length > 0) {
|
||||
this._switch.transport.add(t.tag || t[Symbol.toStringTag], t)
|
||||
} else if (WebSockets.isWebSockets(t)) {
|
||||
// TODO find a cleaner way to signal that a transport is always used
|
||||
// for dialing, even if no listener
|
||||
ws = t
|
||||
}
|
||||
this._transport.push(t)
|
||||
})
|
||||
if (this._config.pubsub.enabled) {
|
||||
this.pubsub && this.pubsub.start()
|
||||
}
|
||||
|
||||
series([
|
||||
(cb) => {
|
||||
this.connectionManager.start()
|
||||
this._switch.start(cb)
|
||||
},
|
||||
(cb) => {
|
||||
if (ws) {
|
||||
// always add dialing on websockets
|
||||
this._switch.transport.add(ws.tag || ws.constructor.name, ws)
|
||||
}
|
||||
// DHT subsystem
|
||||
if (this._config.dht.enabled) {
|
||||
this._dht && this._dht.start()
|
||||
|
||||
// detect which multiaddrs we don't have a transport for and remove them
|
||||
const multiaddrs = this.peerInfo.multiaddrs.toArray()
|
||||
// TODO: this should be modified once random-walk is used as
|
||||
// the other discovery modules
|
||||
this._dht.on('peer', this._onDiscoveryPeer)
|
||||
}
|
||||
|
||||
multiaddrs.forEach((multiaddr) => {
|
||||
if (!multiaddr.toString().match(/\/p2p-circuit($|\/)/) &&
|
||||
!this._transport.find((transport) => transport.filter(multiaddr).length > 0)) {
|
||||
this.peerInfo.multiaddrs.delete(multiaddr)
|
||||
}
|
||||
})
|
||||
cb()
|
||||
},
|
||||
(cb) => {
|
||||
if (this._dht) {
|
||||
this._dht.start(() => {
|
||||
this._dht.on('peer', this._peerDiscovered)
|
||||
cb()
|
||||
})
|
||||
} else {
|
||||
cb()
|
||||
}
|
||||
},
|
||||
(cb) => {
|
||||
if (this._floodSub) {
|
||||
return this._floodSub.start(cb)
|
||||
}
|
||||
cb()
|
||||
},
|
||||
// Peer Discovery
|
||||
(cb) => {
|
||||
if (this._modules.peerDiscovery) {
|
||||
this._setupPeerDiscovery(cb)
|
||||
} else {
|
||||
cb()
|
||||
}
|
||||
}
|
||||
], (err) => {
|
||||
if (err) {
|
||||
log.error(err)
|
||||
this.emit('error', err)
|
||||
return this.state('stop')
|
||||
}
|
||||
this.state('done')
|
||||
})
|
||||
}
|
||||
|
||||
_onStopping () {
|
||||
series([
|
||||
(cb) => {
|
||||
// stop all discoveries before continuing with shutdown
|
||||
parallel(
|
||||
this._discovery.map((d) => {
|
||||
d.removeListener('peer', this._peerDiscovered)
|
||||
return (_cb) => d.stop((err) => {
|
||||
log.error('an error occurred stopping the discovery service', err)
|
||||
_cb()
|
||||
})
|
||||
}),
|
||||
cb
|
||||
)
|
||||
},
|
||||
(cb) => {
|
||||
if (this._floodSub) {
|
||||
return this._floodSub.stop(cb)
|
||||
}
|
||||
cb()
|
||||
},
|
||||
(cb) => {
|
||||
if (this._dht) {
|
||||
this._dht.removeListener('peer', this._peerDiscovered)
|
||||
return this._dht.stop(cb)
|
||||
}
|
||||
cb()
|
||||
},
|
||||
(cb) => {
|
||||
this.connectionManager.stop()
|
||||
this._switch.stop(cb)
|
||||
},
|
||||
(cb) => {
|
||||
// Ensures idempotent restarts, ignore any errors
|
||||
// from removeAll, they're not useful at this point
|
||||
this._switch.transport.removeAll(() => cb())
|
||||
}
|
||||
], (err) => {
|
||||
if (err) {
|
||||
log.error(err)
|
||||
this.emit('error', err)
|
||||
}
|
||||
this.state('done')
|
||||
})
|
||||
// Start metrics if present
|
||||
this.metrics && this.metrics.start()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles discovered peers. Each discovered peer will be emitted via
|
||||
* the `peer:discovery` event. If auto dial is enabled for libp2p
|
||||
* and the current connection count is under the low watermark, the
|
||||
* peer will be dialed.
|
||||
*
|
||||
* TODO: If `peerBook.put` becomes centralized, https://github.com/libp2p/js-libp2p/issues/345,
|
||||
* it would be ideal if only new peers were emitted. Currently, with
|
||||
* other modules adding peers to the `PeerBook` we have no way of knowing
|
||||
* if a peer is new or not, so it has to be emitted.
|
||||
*
|
||||
* Called when libp2p has started and before it returns
|
||||
* @private
|
||||
*/
|
||||
_onDidStart () {
|
||||
this._isStarted = true
|
||||
|
||||
this.connectionManager.start()
|
||||
|
||||
this.peerStore.on('peer', peerInfo => {
|
||||
this.emit('peer:discovery', peerInfo)
|
||||
this._maybeConnect(peerInfo)
|
||||
})
|
||||
|
||||
// Peer discovery
|
||||
this._setupPeerDiscovery()
|
||||
|
||||
// Once we start, emit and dial any peers we may have already discovered
|
||||
for (const peerInfo of this.peerStore.peers.values()) {
|
||||
this.emit('peer:discovery', peerInfo)
|
||||
this._maybeConnect(peerInfo)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever peer discovery services emit `peer` events.
|
||||
* Known peers may be emitted.
|
||||
* @private
|
||||
* @param {PeerInfo} peerInfo
|
||||
*/
|
||||
_peerDiscovered (peerInfo) {
|
||||
if (peerInfo.id.toB58String() === this.peerInfo.id.toB58String()) {
|
||||
_onDiscoveryPeer (peerInfo) {
|
||||
if (peerInfo.id.toString() === this.peerInfo.id.toString()) {
|
||||
log.error(new Error(codes.ERR_DISCOVERED_SELF))
|
||||
return
|
||||
}
|
||||
peerInfo = this.peerBook.put(peerInfo)
|
||||
|
||||
if (!this.isStarted()) return
|
||||
|
||||
this.emit('peer:discovery', peerInfo)
|
||||
this._maybeConnect(peerInfo)
|
||||
this.peerStore.put(peerInfo)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -498,15 +416,17 @@ class Libp2p extends EventEmitter {
|
||||
* @private
|
||||
* @param {PeerInfo} peerInfo
|
||||
*/
|
||||
_maybeConnect (peerInfo) {
|
||||
// If auto dialing is on, check if we should dial
|
||||
if (this._config.peerDiscovery.autoDial === true && !peerInfo.isConnected()) {
|
||||
async _maybeConnect (peerInfo) {
|
||||
// If auto dialing is on and we have no connection to the peer, check if we should dial
|
||||
if (this._config.peerDiscovery.autoDial === true && !this.registrar.getConnection(peerInfo)) {
|
||||
const minPeers = this._options.connectionManager.minPeers || 0
|
||||
if (minPeers > Object.keys(this._switch.connection.connections).length) {
|
||||
log('connecting to discovered peer')
|
||||
this._switch.dialer.connect(peerInfo, (err) => {
|
||||
err && log.error('could not connect to discovered peer', err)
|
||||
})
|
||||
if (minPeers > this.connectionManager._connections.size) {
|
||||
log('connecting to discovered peer %s', peerInfo.id.toString())
|
||||
try {
|
||||
await this.dialer.connectToPeer(peerInfo)
|
||||
} catch (err) {
|
||||
log.error('could not connect to discovered peer', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -515,10 +435,10 @@ class Libp2p extends EventEmitter {
|
||||
* Initializes and starts peer discovery services
|
||||
*
|
||||
* @private
|
||||
* @param {function(Error)} callback
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
_setupPeerDiscovery (callback) {
|
||||
for (const DiscoveryService of this._modules.peerDiscovery) {
|
||||
_setupPeerDiscovery () {
|
||||
const setupService = (DiscoveryService) => {
|
||||
let config = {
|
||||
enabled: true // on by default
|
||||
}
|
||||
@ -529,7 +449,8 @@ class Libp2p extends EventEmitter {
|
||||
config = { ...config, ...this._config.peerDiscovery[DiscoveryService.tag] }
|
||||
}
|
||||
|
||||
if (config.enabled) {
|
||||
if (config.enabled &&
|
||||
!this._discovery.has(DiscoveryService.tag)) { // not already added
|
||||
let discoveryService
|
||||
|
||||
if (typeof DiscoveryService === 'function') {
|
||||
@ -538,14 +459,24 @@ class Libp2p extends EventEmitter {
|
||||
discoveryService = DiscoveryService
|
||||
}
|
||||
|
||||
discoveryService.on('peer', this._peerDiscovered)
|
||||
this._discovery.push(discoveryService)
|
||||
discoveryService.on('peer', this._onDiscoveryPeer)
|
||||
this._discovery.set(DiscoveryService.tag, discoveryService)
|
||||
}
|
||||
}
|
||||
|
||||
each(this._discovery, (d, cb) => {
|
||||
d.start(cb)
|
||||
}, callback)
|
||||
// Discovery modules
|
||||
for (const DiscoveryService of this._modules.peerDiscovery || []) {
|
||||
setupService(DiscoveryService)
|
||||
}
|
||||
|
||||
// Transport modules with discovery
|
||||
for (const Transport of this.transportManager.getTransports()) {
|
||||
if (Transport.discovery) {
|
||||
setupService(Transport.discovery)
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(Array.from(this._discovery.values(), d => d.start()))
|
||||
}
|
||||
}
|
||||
|
||||
@ -554,16 +485,15 @@ module.exports = Libp2p
|
||||
* Like `new Libp2p(options)` except it will create a `PeerInfo`
|
||||
* instance if one is not provided in options.
|
||||
* @param {object} options Libp2p configuration options
|
||||
* @param {function(Error, Libp2p)} callback
|
||||
* @returns {void}
|
||||
* @returns {Libp2p}
|
||||
*/
|
||||
module.exports.createLibp2p = (options, callback) => {
|
||||
module.exports.create = async (options = {}) => {
|
||||
if (options.peerInfo) {
|
||||
return nextTick(callback, null, new Libp2p(options))
|
||||
return new Libp2p(options)
|
||||
}
|
||||
PeerInfo.create((err, peerInfo) => {
|
||||
if (err) return callback(err)
|
||||
options.peerInfo = peerInfo
|
||||
callback(null, new Libp2p(options))
|
||||
})
|
||||
|
||||
const peerInfo = await PeerInfo.create()
|
||||
|
||||
options.peerInfo = peerInfo
|
||||
return new Libp2p(options)
|
||||
}
|
||||
|
67
src/insecure/plaintext.js
Normal file
67
src/insecure/plaintext.js
Normal file
@ -0,0 +1,67 @@
|
||||
'use strict'
|
||||
|
||||
const handshake = require('it-handshake')
|
||||
const lp = require('it-length-prefixed')
|
||||
const PeerId = require('peer-id')
|
||||
const debug = require('debug')
|
||||
const log = debug('libp2p:plaintext')
|
||||
log.error = debug('libp2p:plaintext:error')
|
||||
const { UnexpectedPeerError, InvalidCryptoExchangeError } = require('libp2p-interfaces/src/crypto/errors')
|
||||
|
||||
const { Exchange, KeyType } = require('./proto')
|
||||
const protocol = '/plaintext/2.0.0'
|
||||
|
||||
function lpEncodeExchange (exchange) {
|
||||
const pb = Exchange.encode(exchange)
|
||||
return lp.encode.single(pb)
|
||||
}
|
||||
|
||||
async function encrypt (localId, conn, remoteId) {
|
||||
const shake = handshake(conn)
|
||||
|
||||
// Encode the public key and write it to the remote peer
|
||||
shake.write(lpEncodeExchange({
|
||||
id: localId.toBytes(),
|
||||
pubkey: {
|
||||
Type: KeyType.RSA, // TODO: dont hard code
|
||||
Data: localId.marshalPubKey()
|
||||
}
|
||||
}))
|
||||
|
||||
log('write pubkey exchange to peer %j', remoteId)
|
||||
|
||||
// Get the Exchange message
|
||||
const response = (await lp.decode.fromReader(shake.reader).next()).value
|
||||
const id = Exchange.decode(response.slice())
|
||||
log('read pubkey exchange from peer %j', remoteId)
|
||||
|
||||
let peerId
|
||||
try {
|
||||
peerId = await PeerId.createFromPubKey(id.pubkey.Data)
|
||||
} catch (err) {
|
||||
log.error(err)
|
||||
throw new InvalidCryptoExchangeError('Remote did not provide its public key')
|
||||
}
|
||||
|
||||
if (remoteId && !peerId.isEqual(remoteId)) {
|
||||
throw new UnexpectedPeerError()
|
||||
}
|
||||
|
||||
log('plaintext key exchange completed successfully with peer %j', peerId)
|
||||
|
||||
shake.rest()
|
||||
return {
|
||||
conn: shake.stream,
|
||||
remotePeer: peerId
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
protocol,
|
||||
secureInbound: (localId, conn, remoteId) => {
|
||||
return encrypt(localId, conn, remoteId)
|
||||
},
|
||||
secureOutbound: (localId, conn, remoteId) => {
|
||||
return encrypt(localId, conn, remoteId)
|
||||
}
|
||||
}
|
22
src/insecure/proto.js
Normal file
22
src/insecure/proto.js
Normal file
@ -0,0 +1,22 @@
|
||||
'use strict'
|
||||
|
||||
const protobuf = require('protons')
|
||||
|
||||
module.exports = protobuf(`
|
||||
message Exchange {
|
||||
optional bytes id = 1;
|
||||
optional PublicKey pubkey = 2;
|
||||
}
|
||||
|
||||
enum KeyType {
|
||||
RSA = 0;
|
||||
Ed25519 = 1;
|
||||
Secp256k1 = 2;
|
||||
ECDSA = 3;
|
||||
}
|
||||
|
||||
message PublicKey {
|
||||
required KeyType Type = 1;
|
||||
required bytes Data = 2;
|
||||
}
|
||||
`)
|
247
src/metrics/index.js
Normal file
247
src/metrics/index.js
Normal file
@ -0,0 +1,247 @@
|
||||
'use strict'
|
||||
|
||||
const mergeOptions = require('merge-options')
|
||||
const pipe = require('it-pipe')
|
||||
const { tap } = require('streaming-iterables')
|
||||
const oldPeerLRU = require('./old-peers')
|
||||
const { METRICS: defaultOptions } = require('../constants')
|
||||
const Stats = require('./stats')
|
||||
|
||||
const initialCounters = [
|
||||
'dataReceived',
|
||||
'dataSent'
|
||||
]
|
||||
|
||||
const directionToEvent = {
|
||||
in: 'dataReceived',
|
||||
out: 'dataSent'
|
||||
}
|
||||
|
||||
class Metrics {
|
||||
/**
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {number} options.computeThrottleMaxQueueSize
|
||||
* @param {number} options.computeThrottleTimeout
|
||||
* @param {Array<number>} options.movingAverageIntervals
|
||||
* @param {number} options.maxOldPeersRetention
|
||||
*/
|
||||
constructor (options) {
|
||||
this._options = mergeOptions(defaultOptions, options)
|
||||
this._globalStats = new Stats(initialCounters, this._options)
|
||||
this._peerStats = new Map()
|
||||
this._protocolStats = new Map()
|
||||
this._oldPeers = oldPeerLRU(this._options.maxOldPeersRetention)
|
||||
this._running = false
|
||||
this._onMessage = this._onMessage.bind(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be called for stats to saved. Any data pushed for tracking
|
||||
* will be ignored.
|
||||
*/
|
||||
start () {
|
||||
this._running = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops all averages timers and prevents new data from being tracked.
|
||||
* Once `stop` is called, `start` must be called to resume stats tracking.
|
||||
*/
|
||||
stop () {
|
||||
this._running = false
|
||||
this._globalStats.stop()
|
||||
for (const stats of this._peerStats.values()) {
|
||||
stats.stop()
|
||||
}
|
||||
for (const stats of this._protocolStats.values()) {
|
||||
stats.stop()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the global `Stats` object
|
||||
* @returns {Stats}
|
||||
*/
|
||||
get global () {
|
||||
return this._globalStats
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of `PeerId` strings currently being tracked
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
get peers () {
|
||||
return Array.from(this._peerStats.keys())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the `Stats` object for the given `PeerId` whether it
|
||||
* is a live peer, or in the disconnected peer LRU cache.
|
||||
* @param {PeerId} peerId
|
||||
* @returns {Stats}
|
||||
*/
|
||||
forPeer (peerId) {
|
||||
const idString = peerId.toString()
|
||||
return this._peerStats.get(idString) || this._oldPeers.get(idString)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all protocol strings currently being tracked.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
get protocols () {
|
||||
return Array.from(this._protocolStats.keys())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the `Stats` object for the given `protocol`.
|
||||
* @param {string} protocol
|
||||
* @returns {Stats}
|
||||
*/
|
||||
forProtocol (protocol) {
|
||||
return this._protocolStats.get(protocol)
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when all connections to a given peer
|
||||
* have closed. The `Stats` collection for the peer will
|
||||
* be stopped and moved to an LRU for temporary retention.
|
||||
* @param {PeerId} peerId
|
||||
*/
|
||||
onPeerDisconnected (peerId) {
|
||||
const idString = peerId.toString()
|
||||
const peerStats = this._peerStats.get(idString)
|
||||
if (peerStats) {
|
||||
peerStats.stop()
|
||||
this._peerStats.delete(idString)
|
||||
this._oldPeers.set(idString, peerStats)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the metadata for a message and tracks it in the
|
||||
* appropriate categories. If the protocol is present, protocol
|
||||
* stats will also be tracked.
|
||||
*
|
||||
* @private
|
||||
* @param {object} params
|
||||
* @param {PeerId} params.remotePeer Remote peer
|
||||
* @param {string} [params.protocol] Protocol string the stream is running
|
||||
* @param {string} params.direction One of ['in','out']
|
||||
* @param {number} params.dataLength Size of the message
|
||||
* @returns {void}
|
||||
*/
|
||||
_onMessage ({ remotePeer, protocol, direction, dataLength }) {
|
||||
if (!this._running) return
|
||||
|
||||
const key = directionToEvent[direction]
|
||||
|
||||
let peerStats = this.forPeer(remotePeer)
|
||||
if (!peerStats) {
|
||||
peerStats = new Stats(initialCounters, this._options)
|
||||
this._peerStats.set(remotePeer.toString(), peerStats)
|
||||
}
|
||||
|
||||
// Peer and global stats
|
||||
peerStats.push(key, dataLength)
|
||||
this._globalStats.push(key, dataLength)
|
||||
|
||||
// Protocol specific stats
|
||||
if (protocol) {
|
||||
let protocolStats = this.forProtocol(protocol)
|
||||
if (!protocolStats) {
|
||||
protocolStats = new Stats(initialCounters, this._options)
|
||||
this._protocolStats.set(protocol, protocolStats)
|
||||
}
|
||||
protocolStats.push(key, dataLength)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the `PeerId` string with the given `peerId`.
|
||||
* If stats are already being tracked for the given `peerId`, the
|
||||
* placeholder stats will be merged with the existing stats.
|
||||
* @param {string} placeholder A peerId string
|
||||
* @param {PeerId} peerId
|
||||
*/
|
||||
updatePlaceholder (placeholder, peerId) {
|
||||
if (!this._running) return
|
||||
const placeholderStats = this.forPeer(placeholder)
|
||||
const peerIdString = peerId.toString()
|
||||
const existingStats = this.forPeer(peerId)
|
||||
let mergedStats = placeholderStats
|
||||
|
||||
// If we already have stats, merge the two
|
||||
if (existingStats) {
|
||||
// If existing, merge
|
||||
mergedStats = Metrics.mergeStats(existingStats, mergedStats)
|
||||
// Attempt to delete from the old peers list just in case it was tracked there
|
||||
this._oldPeers.delete(peerIdString)
|
||||
}
|
||||
|
||||
this._peerStats.delete(placeholder.toString())
|
||||
this._peerStats.set(peerIdString, mergedStats)
|
||||
mergedStats.start()
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks data running through a given Duplex Iterable `stream`. If
|
||||
* the `peerId` is not provided, a placeholder string will be created and
|
||||
* returned. This allows lazy tracking of a peer when the peer is not yet known.
|
||||
* When the `PeerId` is known, `Metrics.updatePlaceholder` should be called
|
||||
* with the placeholder string returned from here, and the known `PeerId`.
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {{ sink: function(*), source: function() }} options.stream A duplex iterable stream
|
||||
* @param {PeerId} [options.peerId] The id of the remote peer that's connected
|
||||
* @param {string} [options.protocol] The protocol the stream is running
|
||||
* @returns {string} The peerId string or placeholder string
|
||||
*/
|
||||
trackStream ({ stream, remotePeer, protocol }) {
|
||||
const metrics = this
|
||||
const _source = stream.source
|
||||
stream.source = tap(chunk => metrics._onMessage({
|
||||
remotePeer,
|
||||
protocol,
|
||||
direction: 'in',
|
||||
dataLength: chunk.length
|
||||
}))(_source)
|
||||
|
||||
const _sink = stream.sink
|
||||
stream.sink = source => {
|
||||
pipe(
|
||||
source,
|
||||
tap(chunk => metrics._onMessage({
|
||||
remotePeer,
|
||||
protocol,
|
||||
direction: 'out',
|
||||
dataLength: chunk.length
|
||||
})),
|
||||
_sink
|
||||
)
|
||||
}
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges `other` into `target`. `target` will be modified
|
||||
* and returned.
|
||||
* @param {Stats} target
|
||||
* @param {Stats} other
|
||||
* @returns {Stats}
|
||||
*/
|
||||
static mergeStats (target, other) {
|
||||
target.stop()
|
||||
other.stop()
|
||||
|
||||
// Merge queues
|
||||
target._queue = [...target._queue, ...other._queue]
|
||||
|
||||
// TODO: how to merge moving averages?
|
||||
return target
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Metrics
|
15
src/metrics/old-peers.js
Normal file
15
src/metrics/old-peers.js
Normal file
@ -0,0 +1,15 @@
|
||||
'use strict'
|
||||
|
||||
const LRU = require('hashlru')
|
||||
|
||||
/**
|
||||
* Creates and returns a Least Recently Used Cache
|
||||
*
|
||||
* @param {Number} maxSize
|
||||
* @returns {LRUCache}
|
||||
*/
|
||||
module.exports = (maxSize) => {
|
||||
const patched = LRU(maxSize)
|
||||
patched.delete = patched.remove
|
||||
return patched
|
||||
}
|
264
src/metrics/stats.js
Normal file
264
src/metrics/stats.js
Normal file
@ -0,0 +1,264 @@
|
||||
'use strict'
|
||||
|
||||
const EventEmitter = require('events')
|
||||
const Big = require('bignumber.js')
|
||||
const MovingAverage = require('moving-average')
|
||||
const retimer = require('retimer')
|
||||
|
||||
/**
|
||||
* A queue based manager for stat processing
|
||||
*
|
||||
* @param {Array<string>} initialCounters
|
||||
* @param {any} options
|
||||
*/
|
||||
class Stats extends EventEmitter {
|
||||
constructor (initialCounters, options) {
|
||||
super()
|
||||
|
||||
this._options = options
|
||||
this._queue = []
|
||||
this._stats = {}
|
||||
|
||||
this._frequencyLastTime = Date.now()
|
||||
this._frequencyAccumulators = {}
|
||||
this._movingAverages = {}
|
||||
|
||||
this._update = this._update.bind(this)
|
||||
|
||||
const intervals = this._options.movingAverageIntervals
|
||||
|
||||
for (var i = 0; i < initialCounters.length; i++) {
|
||||
var key = initialCounters[i]
|
||||
this._stats[key] = Big(0)
|
||||
this._movingAverages[key] = {}
|
||||
for (var k = 0; k < intervals.length; k++) {
|
||||
var interval = intervals[k]
|
||||
var ma = this._movingAverages[key][interval] = MovingAverage(interval)
|
||||
ma.push(this._frequencyLastTime, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the internal timer if there are items in the queue. This
|
||||
* should only need to be called if `Stats.stop` was previously called, as
|
||||
* `Stats.push` will also start the processing.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
start () {
|
||||
if (this._queue.length) {
|
||||
this._resetComputeTimeout()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops processing and computing of stats by clearing the internal
|
||||
* timer.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
stop () {
|
||||
if (this._timeout) {
|
||||
this._timeout.clear()
|
||||
this._timeout = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a clone of the current stats.
|
||||
*
|
||||
* @returns {Map<string, Stat>}
|
||||
*/
|
||||
get snapshot () {
|
||||
return Object.assign({}, this._stats)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a clone of the internal movingAverages
|
||||
*
|
||||
* @returns {Array<MovingAverage>}
|
||||
*/
|
||||
get movingAverages () {
|
||||
return Object.assign({}, this._movingAverages)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a plain JSON object of the stats
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
toJSON () {
|
||||
const snapshot = this.snapshot
|
||||
const movingAverages = this.movingAverages
|
||||
const data = {
|
||||
dataReceived: snapshot.dataReceived.toString(),
|
||||
dataSent: snapshot.dataSent.toString(),
|
||||
movingAverages: {}
|
||||
}
|
||||
|
||||
const counters = Object.keys(movingAverages)
|
||||
for (const key of counters) {
|
||||
data.movingAverages[key] = {}
|
||||
for (const interval of Object.keys(movingAverages[key])) {
|
||||
data.movingAverages[key][interval] = movingAverages[key][interval].movingAverage()
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes the given operation data to the queue, along with the
|
||||
* current Timestamp, then resets the update timer.
|
||||
*
|
||||
* @param {string} counter
|
||||
* @param {number} inc
|
||||
* @returns {void}
|
||||
*/
|
||||
push (counter, inc) {
|
||||
this._queue.push([counter, inc, Date.now()])
|
||||
this._resetComputeTimeout()
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the timeout for triggering updates.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_resetComputeTimeout () {
|
||||
if (this._timeout) {
|
||||
this._timeout.reschedule(this._nextTimeout())
|
||||
} else {
|
||||
this._timeout = retimer(this._update, this._nextTimeout())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates and returns the timeout for the next update based on
|
||||
* the urgency of the update.
|
||||
*
|
||||
* @private
|
||||
* @returns {number}
|
||||
*/
|
||||
_nextTimeout () {
|
||||
// calculate the need for an update, depending on the queue length
|
||||
const urgency = this._queue.length / this._options.computeThrottleMaxQueueSize
|
||||
const timeout = Math.max(this._options.computeThrottleTimeout * (1 - urgency), 0)
|
||||
return timeout
|
||||
}
|
||||
|
||||
/**
|
||||
* If there are items in the queue, they will will be processed and
|
||||
* the frequency for all items will be updated based on the Timestamp
|
||||
* of the last item in the queue. The `update` event will also be emitted
|
||||
* with the latest stats.
|
||||
*
|
||||
* If there are no items in the queue, no action is taken.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_update () {
|
||||
this._timeout = null
|
||||
if (this._queue.length) {
|
||||
let last
|
||||
for (last of this._queue) {
|
||||
this._applyOp(last)
|
||||
}
|
||||
this._queue = []
|
||||
|
||||
this._updateFrequency(last[2]) // contains timestamp of last op
|
||||
|
||||
this.emit('update', this._stats)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For each key in the stats, the frequency and moving averages
|
||||
* will be updated via Stats._updateFrequencyFor based on the time
|
||||
* difference between calls to this method.
|
||||
*
|
||||
* @private
|
||||
* @param {Timestamp} latestTime
|
||||
* @returns {void}
|
||||
*/
|
||||
_updateFrequency (latestTime) {
|
||||
const timeDiff = latestTime - this._frequencyLastTime
|
||||
|
||||
Object.keys(this._stats).forEach((key) => {
|
||||
this._updateFrequencyFor(key, timeDiff, latestTime)
|
||||
})
|
||||
|
||||
this._frequencyLastTime = latestTime
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the `movingAverages` for the given `key` and also
|
||||
* resets the `frequencyAccumulator` for the `key`.
|
||||
*
|
||||
* @private
|
||||
* @param {string} key
|
||||
* @param {number} timeDiffMS Time in milliseconds
|
||||
* @param {Timestamp} latestTime Time in ticks
|
||||
* @returns {void}
|
||||
*/
|
||||
_updateFrequencyFor (key, timeDiffMS, latestTime) {
|
||||
const count = this._frequencyAccumulators[key] || 0
|
||||
this._frequencyAccumulators[key] = 0
|
||||
// if `timeDiff` is zero, `hz` becomes Infinity, so we fallback to 1ms
|
||||
const safeTimeDiff = timeDiffMS || 1
|
||||
const hz = (count / safeTimeDiff) * 1000
|
||||
|
||||
let movingAverages = this._movingAverages[key]
|
||||
if (!movingAverages) {
|
||||
movingAverages = this._movingAverages[key] = {}
|
||||
}
|
||||
|
||||
const intervals = this._options.movingAverageIntervals
|
||||
|
||||
for (var i = 0; i < intervals.length; i++) {
|
||||
var movingAverageInterval = intervals[i]
|
||||
var movingAverage = movingAverages[movingAverageInterval]
|
||||
if (!movingAverage) {
|
||||
movingAverage = movingAverages[movingAverageInterval] = MovingAverage(movingAverageInterval)
|
||||
}
|
||||
movingAverage.push(latestTime, hz)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For the given operation, `op`, the stats and `frequencyAccumulator`
|
||||
* will be updated or initialized if they don't already exist.
|
||||
*
|
||||
* @private
|
||||
* @param {Array<string, number>} op
|
||||
* @throws {InvalidNumber}
|
||||
* @returns {void}
|
||||
*/
|
||||
_applyOp (op) {
|
||||
const key = op[0]
|
||||
const inc = op[1]
|
||||
|
||||
if (typeof inc !== 'number') {
|
||||
throw new Error('invalid increment number:', inc)
|
||||
}
|
||||
|
||||
let n
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(this._stats, key)) {
|
||||
n = this._stats[key] = Big(0)
|
||||
} else {
|
||||
n = this._stats[key]
|
||||
}
|
||||
this._stats[key] = n.plus(inc)
|
||||
|
||||
if (!this._frequencyAccumulators[key]) {
|
||||
this._frequencyAccumulators[key] = 0
|
||||
}
|
||||
this._frequencyAccumulators[key] += inc
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Stats
|
@ -1,7 +1,7 @@
|
||||
'use strict'
|
||||
|
||||
const tryEach = require('async/tryEach')
|
||||
const errCode = require('err-code')
|
||||
const pAny = require('p-any')
|
||||
|
||||
module.exports = (node) => {
|
||||
const routers = node._modules.peerRouting || []
|
||||
@ -16,43 +16,25 @@ module.exports = (node) => {
|
||||
* Iterates over all peer routers in series to find the given peer.
|
||||
*
|
||||
* @param {String} id The id of the peer to find
|
||||
* @param {object} options
|
||||
* @param {number} options.maxTimeout How long the query should run
|
||||
* @param {function(Error, Result<Array>)} callback
|
||||
* @returns {void}
|
||||
* @param {object} [options]
|
||||
* @param {number} [options.timeout] How long the query should run
|
||||
* @returns {Promise<PeerInfo>}
|
||||
*/
|
||||
findPeer: (id, options, callback) => {
|
||||
if (typeof options === 'function') {
|
||||
callback = options
|
||||
options = {}
|
||||
}
|
||||
|
||||
findPeer: async (id, options) => { // eslint-disable-line require-await
|
||||
if (!routers.length) {
|
||||
callback(errCode(new Error('No peer routers available'), 'NO_ROUTERS_AVAILABLE'))
|
||||
throw errCode(new Error('No peer routers available'), 'NO_ROUTERS_AVAILABLE')
|
||||
}
|
||||
|
||||
const tasks = routers.map((router) => {
|
||||
return (cb) => router.findPeer(id, options, (err, result) => {
|
||||
if (err) {
|
||||
return cb(err)
|
||||
}
|
||||
return pAny(routers.map(async (router) => {
|
||||
const result = await router.findPeer(id, options)
|
||||
|
||||
// If we don't have a result, we need to provide an error to keep trying
|
||||
if (!result || Object.keys(result).length === 0) {
|
||||
return cb(errCode(new Error('not found'), 'NOT_FOUND'), null)
|
||||
}
|
||||
|
||||
cb(null, result)
|
||||
})
|
||||
})
|
||||
|
||||
tryEach(tasks, (err, results) => {
|
||||
if (err && err.code !== 'NOT_FOUND') {
|
||||
return callback(err)
|
||||
// If we don't have a result, we need to provide an error to keep trying
|
||||
if (!result || Object.keys(result).length === 0) {
|
||||
throw errCode(new Error('not found'), 'NOT_FOUND')
|
||||
}
|
||||
results = results || []
|
||||
callback(null, results)
|
||||
})
|
||||
|
||||
return result
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
3
src/peer-store/README.md
Normal file
3
src/peer-store/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Peerstore
|
||||
|
||||
WIP
|
236
src/peer-store/index.js
Normal file
236
src/peer-store/index.js
Normal file
@ -0,0 +1,236 @@
|
||||
'use strict'
|
||||
|
||||
const assert = require('assert')
|
||||
const debug = require('debug')
|
||||
const log = debug('libp2p:peer-store')
|
||||
log.error = debug('libp2p:peer-store:error')
|
||||
|
||||
const { EventEmitter } = require('events')
|
||||
|
||||
const PeerId = require('peer-id')
|
||||
const PeerInfo = require('peer-info')
|
||||
|
||||
/**
|
||||
* Responsible for managing known peers, as well as their addresses and metadata
|
||||
* @fires PeerStore#peer Emitted when a peer is connected to this node
|
||||
* @fires PeerStore#change:protocols
|
||||
* @fires PeerStore#change:multiaddrs
|
||||
*/
|
||||
class PeerStore extends EventEmitter {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
/**
|
||||
* Map of peers
|
||||
*
|
||||
* @type {Map<string, PeerInfo>}
|
||||
*/
|
||||
this.peers = new Map()
|
||||
|
||||
// TODO: Track ourselves. We should split `peerInfo` up into its pieces so we get better
|
||||
// control and observability. This will be the initial step for removing PeerInfo
|
||||
// https://github.com/libp2p/go-libp2p-core/blob/master/peerstore/peerstore.go
|
||||
// this.addressBook = new Map()
|
||||
// this.protoBook = new Map()
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the peerInfo of a new peer.
|
||||
* If already exist, its info is updated. If `silent` is set to
|
||||
* true, no 'peer' event will be emitted. This can be useful if you
|
||||
* are already in the process of dialing the peer. The peer is technically
|
||||
* known, but may not have been added to the PeerStore yet.
|
||||
* @param {PeerInfo} peerInfo
|
||||
* @param {object} [options]
|
||||
* @param {boolean} [options.silent] (Default=false)
|
||||
* @return {PeerInfo}
|
||||
*/
|
||||
put (peerInfo, options = { silent: false }) {
|
||||
assert(PeerInfo.isPeerInfo(peerInfo), 'peerInfo must be an instance of peer-info')
|
||||
|
||||
let peer
|
||||
// Already know the peer?
|
||||
if (this.has(peerInfo.id)) {
|
||||
peer = this.update(peerInfo)
|
||||
} else {
|
||||
peer = this.add(peerInfo)
|
||||
|
||||
// Emit the peer if silent = false
|
||||
!options.silent && this.emit('peer', peerInfo)
|
||||
}
|
||||
return peer
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new peer to the store.
|
||||
* @param {PeerInfo} peerInfo
|
||||
* @return {PeerInfo}
|
||||
*/
|
||||
add (peerInfo) {
|
||||
assert(PeerInfo.isPeerInfo(peerInfo), 'peerInfo must be an instance of peer-info')
|
||||
|
||||
// Create new instance and add values to it
|
||||
const newPeerInfo = new PeerInfo(peerInfo.id)
|
||||
|
||||
peerInfo.multiaddrs.forEach((ma) => newPeerInfo.multiaddrs.add(ma))
|
||||
peerInfo.protocols.forEach((p) => newPeerInfo.protocols.add(p))
|
||||
|
||||
const connectedMa = peerInfo.isConnected()
|
||||
connectedMa && newPeerInfo.connect(connectedMa)
|
||||
|
||||
const peerProxy = new Proxy(newPeerInfo, {
|
||||
set: (obj, prop, value) => {
|
||||
if (prop === 'multiaddrs') {
|
||||
this.emit('change:multiaddrs', {
|
||||
peerInfo: obj,
|
||||
multiaddrs: value.toArray()
|
||||
})
|
||||
} else if (prop === 'protocols') {
|
||||
this.emit('change:protocols', {
|
||||
peerInfo: obj,
|
||||
protocols: Array.from(value)
|
||||
})
|
||||
}
|
||||
return Reflect.set(...arguments)
|
||||
}
|
||||
})
|
||||
|
||||
this.peers.set(peerInfo.id.toB58String(), peerProxy)
|
||||
return peerProxy
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an already known peer.
|
||||
* @param {PeerInfo} peerInfo
|
||||
* @return {PeerInfo}
|
||||
*/
|
||||
update (peerInfo) {
|
||||
assert(PeerInfo.isPeerInfo(peerInfo), 'peerInfo must be an instance of peer-info')
|
||||
const id = peerInfo.id.toB58String()
|
||||
const recorded = this.peers.get(id)
|
||||
|
||||
// pass active connection state
|
||||
const ma = peerInfo.isConnected()
|
||||
if (ma) {
|
||||
recorded.connect(ma)
|
||||
}
|
||||
|
||||
// Verify new multiaddrs
|
||||
// TODO: better track added and removed multiaddrs
|
||||
const multiaddrsIntersection = [
|
||||
...recorded.multiaddrs.toArray()
|
||||
].filter((m) => peerInfo.multiaddrs.has(m))
|
||||
|
||||
if (multiaddrsIntersection.length !== peerInfo.multiaddrs.size ||
|
||||
multiaddrsIntersection.length !== recorded.multiaddrs.size) {
|
||||
for (const ma of peerInfo.multiaddrs.toArray()) {
|
||||
recorded.multiaddrs.add(ma)
|
||||
}
|
||||
|
||||
this.emit('change:multiaddrs', {
|
||||
peerInfo: recorded,
|
||||
multiaddrs: recorded.multiaddrs.toArray()
|
||||
})
|
||||
}
|
||||
|
||||
// Update protocols
|
||||
// TODO: better track added and removed protocols
|
||||
const protocolsIntersection = new Set(
|
||||
[...recorded.protocols].filter((p) => peerInfo.protocols.has(p))
|
||||
)
|
||||
|
||||
if (protocolsIntersection.size !== peerInfo.protocols.size ||
|
||||
protocolsIntersection.size !== recorded.protocols.size) {
|
||||
for (const protocol of peerInfo.protocols) {
|
||||
recorded.protocols.add(protocol)
|
||||
}
|
||||
|
||||
this.emit('change:protocols', {
|
||||
peerInfo: recorded,
|
||||
protocols: Array.from(recorded.protocols)
|
||||
})
|
||||
}
|
||||
|
||||
// Add the public key if missing
|
||||
if (!recorded.id.pubKey && peerInfo.id.pubKey) {
|
||||
recorded.id.pubKey = peerInfo.id.pubKey
|
||||
}
|
||||
|
||||
return recorded
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the info to the given id.
|
||||
* @param {PeerId|string} peerId b58str id
|
||||
* @returns {PeerInfo}
|
||||
*/
|
||||
get (peerId) {
|
||||
// TODO: deprecate this and just accept `PeerId` instances
|
||||
if (PeerId.isPeerId(peerId)) {
|
||||
peerId = peerId.toB58String()
|
||||
}
|
||||
|
||||
return this.peers.get(peerId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Has the info to the given id.
|
||||
* @param {PeerId|string} peerId b58str id
|
||||
* @returns {boolean}
|
||||
*/
|
||||
has (peerId) {
|
||||
// TODO: deprecate this and just accept `PeerId` instances
|
||||
if (PeerId.isPeerId(peerId)) {
|
||||
peerId = peerId.toB58String()
|
||||
}
|
||||
|
||||
return this.peers.has(peerId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the Peer with the matching `peerId` from the PeerStore
|
||||
* @param {PeerId|string} peerId b58str id
|
||||
* @returns {boolean} true if found and removed
|
||||
*/
|
||||
remove (peerId) {
|
||||
// TODO: deprecate this and just accept `PeerId` instances
|
||||
if (PeerId.isPeerId(peerId)) {
|
||||
peerId = peerId.toB58String()
|
||||
}
|
||||
|
||||
return this.peers.delete(peerId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Completely replaces the existing peers metadata with the given `peerInfo`
|
||||
* @param {PeerInfo} peerInfo
|
||||
* @returns {void}
|
||||
*/
|
||||
replace (peerInfo) {
|
||||
assert(PeerInfo.isPeerInfo(peerInfo), 'peerInfo must be an instance of peer-info')
|
||||
|
||||
this.remove(peerInfo.id.toB58String())
|
||||
this.add(peerInfo)
|
||||
|
||||
// This should be cleaned up in PeerStore v2
|
||||
this.emit('change:multiaddrs', {
|
||||
peerInfo,
|
||||
multiaddrs: peerInfo.multiaddrs.toArray()
|
||||
})
|
||||
this.emit('change:protocols', {
|
||||
peerInfo,
|
||||
protocols: Array.from(peerInfo.protocols)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the known multiaddrs for a given `PeerInfo`
|
||||
* @param {PeerInfo} peer
|
||||
* @returns {Array<Multiaddr>}
|
||||
*/
|
||||
multiaddrsForPeer (peer) {
|
||||
return this.put(peer, true).multiaddrs.toArray()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PeerStore
|
18
src/ping/README.md
Normal file
18
src/ping/README.md
Normal file
@ -0,0 +1,18 @@
|
||||
libp2p-ping JavaScript Implementation
|
||||
=====================================
|
||||
|
||||
> IPFS ping protocol JavaScript implementation
|
||||
|
||||
**Note**: git history prior to merging into js-libp2p can be found in the original repository, https://github.com/libp2p/js-libp2p-ping.
|
||||
|
||||
## Usage
|
||||
|
||||
```javascript
|
||||
var Ping = require('libp2p/src/ping')
|
||||
|
||||
Ping.mount(libp2p) // Enable this peer to echo Ping requests
|
||||
|
||||
const latency = await Ping(libp2p, peerDst)
|
||||
|
||||
Ping.unmount(libp2p)
|
||||
```
|
6
src/ping/constants.js
Normal file
6
src/ping/constants.js
Normal file
@ -0,0 +1,6 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = {
|
||||
PROTOCOL: '/ipfs/ping/1.0.0',
|
||||
PING_LENGTH: 32
|
||||
}
|
62
src/ping/index.js
Normal file
62
src/ping/index.js
Normal file
@ -0,0 +1,62 @@
|
||||
'use strict'
|
||||
|
||||
const debug = require('debug')
|
||||
const log = debug('libp2p-ping')
|
||||
log.error = debug('libp2p-ping:error')
|
||||
const errCode = require('err-code')
|
||||
|
||||
const crypto = require('libp2p-crypto')
|
||||
const pipe = require('it-pipe')
|
||||
const { toBuffer } = require('it-buffer')
|
||||
const { collect } = require('streaming-iterables')
|
||||
|
||||
const { PROTOCOL, PING_LENGTH } = require('./constants')
|
||||
|
||||
/**
|
||||
* Ping a given peer and wait for its response, getting the operation latency.
|
||||
* @param {Libp2p} node
|
||||
* @param {PeerInfo} peer
|
||||
* @returns {Promise<Number>}
|
||||
*/
|
||||
async function ping (node, peer) {
|
||||
log('dialing %s to %s', PROTOCOL, peer.id.toB58String())
|
||||
|
||||
const { stream } = await node.dialProtocol(peer, PROTOCOL)
|
||||
|
||||
const start = new Date()
|
||||
const data = crypto.randomBytes(PING_LENGTH)
|
||||
|
||||
const [result] = await pipe(
|
||||
[data],
|
||||
stream,
|
||||
toBuffer,
|
||||
collect
|
||||
)
|
||||
const end = Date.now()
|
||||
|
||||
if (!data.equals(result)) {
|
||||
throw errCode(new Error('Received wrong ping ack'), 'ERR_WRONG_PING_ACK')
|
||||
}
|
||||
|
||||
return end - start
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe ping protocol handler.
|
||||
* @param {Libp2p} node
|
||||
*/
|
||||
function mount (node) {
|
||||
node.handle(PROTOCOL, ({ stream }) => pipe(stream, stream))
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe ping protocol handler.
|
||||
* @param {Libp2p} node
|
||||
*/
|
||||
function unmount (node) {
|
||||
node.unhandle(PROTOCOL)
|
||||
}
|
||||
|
||||
exports = module.exports = ping
|
||||
exports.mount = mount
|
||||
exports.unmount = unmount
|
13
src/ping/util.js
Normal file
13
src/ping/util.js
Normal file
@ -0,0 +1,13 @@
|
||||
'use strict'
|
||||
|
||||
const crypto = require('libp2p-crypto')
|
||||
const constants = require('./constants')
|
||||
|
||||
exports = module.exports
|
||||
|
||||
exports.rnd = (length) => {
|
||||
if (!length) {
|
||||
length = constants.PING_LENGTH
|
||||
}
|
||||
return crypto.randomBytes(length)
|
||||
}
|
70
src/pnet/README.md
Normal file
70
src/pnet/README.md
Normal file
@ -0,0 +1,70 @@
|
||||
js-libp2p-pnet
|
||||
==================
|
||||
|
||||
> Connection protection management for libp2p leveraging PSK encryption via XSalsa20.
|
||||
|
||||
**Note**: git history prior to merging into js-libp2p can be found in the original repository, https://github.com/libp2p/js-libp2p-pnet.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [js-libp2p-pnet](#js-libp2p-pnet)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Usage](#usage)
|
||||
- [Examples](#examples)
|
||||
- [Private Shared Keys](#private-shared-keys)
|
||||
- [PSK Generation](#psk-generation)
|
||||
- [From libp2p-pnet](#from-libp2p-pnet)
|
||||
- [From a module using libp2p](#from-a-module-using-libp2p)
|
||||
- [Programmatically](#programmatically)
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
const Protector = require('libp2p-pnet')
|
||||
const protector = new Protector(swarmKeyBuffer)
|
||||
const privateConnection = protector.protect(myPublicConnection, (err) => { })
|
||||
```
|
||||
|
||||
### Examples
|
||||
[Private Networks with IPFS](../../examples/pnet-ipfs)
|
||||
|
||||
### Private Shared Keys
|
||||
|
||||
Private Shared Keys are expected to be in the following format:
|
||||
|
||||
```
|
||||
/key/swarm/psk/1.0.0/
|
||||
/base16/
|
||||
dffb7e3135399a8b1612b2aaca1c36a3a8ac2cd0cca51ceeb2ced87d308cac6d
|
||||
```
|
||||
|
||||
### PSK Generation
|
||||
|
||||
A utility method has been created to generate a key for your private network. You can
|
||||
use one of the methods below to generate your key.
|
||||
|
||||
#### From libp2p-pnet
|
||||
|
||||
If you have libp2p locally, you can run the following from the projects root.
|
||||
|
||||
```sh
|
||||
node ./src/pnet/key-generator.js > swarm.key
|
||||
```
|
||||
|
||||
#### From a module using libp2p
|
||||
|
||||
If you have a module locally that depends on libp2p, you can run the following from
|
||||
that project, assuming the node_modules are installed.
|
||||
|
||||
```sh
|
||||
node -e "require('libp2p/src/pnet').generate(process.stdout)" > swarm.key
|
||||
```
|
||||
|
||||
#### Programmatically
|
||||
|
||||
```js
|
||||
const writeKey = require('libp2p/src/pnet').generate
|
||||
const swarmKey = Buffer.alloc(95)
|
||||
writeKey(swarmKey)
|
||||
fs.writeFileSync('swarm.key', swarmKey)
|
||||
```
|
78
src/pnet/crypto.js
Normal file
78
src/pnet/crypto.js
Normal file
@ -0,0 +1,78 @@
|
||||
'use strict'
|
||||
|
||||
const debug = require('debug')
|
||||
const Errors = require('./errors')
|
||||
const xsalsa20 = require('xsalsa20')
|
||||
const KEY_LENGTH = require('./key-generator').KEY_LENGTH
|
||||
|
||||
const log = debug('libp2p:pnet')
|
||||
log.trace = debug('libp2p:pnet:trace')
|
||||
log.error = debug('libp2p:pnet:err')
|
||||
|
||||
/**
|
||||
* Creates a stream iterable to encrypt messages in a private network
|
||||
*
|
||||
* @param {Buffer} nonce The nonce to use in encryption
|
||||
* @param {Buffer} psk The private shared key to use in encryption
|
||||
* @returns {*} a through iterable
|
||||
*/
|
||||
module.exports.createBoxStream = (nonce, psk) => {
|
||||
const xor = xsalsa20(nonce, psk)
|
||||
return (source) => (async function * () {
|
||||
for await (const chunk of source) {
|
||||
yield Buffer.from(xor.update(chunk.slice()))
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a stream iterable to decrypt messages in a private network
|
||||
*
|
||||
* @param {Buffer} nonce The nonce of the remote peer
|
||||
* @param {Buffer} psk The private shared key to use in decryption
|
||||
* @returns {*} a through iterable
|
||||
*/
|
||||
module.exports.createUnboxStream = (nonce, psk) => {
|
||||
return (source) => (async function * () {
|
||||
const xor = xsalsa20(nonce, psk)
|
||||
log.trace('Decryption enabled')
|
||||
|
||||
for await (const chunk of source) {
|
||||
yield Buffer.from(xor.update(chunk.slice()))
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the version 1 psk from the given Buffer
|
||||
*
|
||||
* @param {Buffer} pskBuffer
|
||||
* @throws {INVALID_PSK}
|
||||
* @returns {Object} The PSK metadata (tag, codecName, psk)
|
||||
*/
|
||||
module.exports.decodeV1PSK = (pskBuffer) => {
|
||||
try {
|
||||
// This should pull from multibase/multicodec to allow for
|
||||
// more encoding flexibility. Ideally we'd consume the codecs
|
||||
// from the buffer line by line to evaluate the next line
|
||||
// programmatically instead of making assumptions about the
|
||||
// encodings of each line.
|
||||
const metadata = pskBuffer.toString().split(/(?:\r\n|\r|\n)/g)
|
||||
const pskTag = metadata.shift()
|
||||
const codec = metadata.shift()
|
||||
const psk = Buffer.from(metadata.shift(), 'hex')
|
||||
|
||||
if (psk.byteLength !== KEY_LENGTH) {
|
||||
throw new Error(Errors.INVALID_PSK)
|
||||
}
|
||||
|
||||
return {
|
||||
tag: pskTag,
|
||||
codecName: codec,
|
||||
psk: psk
|
||||
}
|
||||
} catch (err) {
|
||||
log.error(err)
|
||||
throw new Error(Errors.INVALID_PSK)
|
||||
}
|
||||
}
|
7
src/pnet/errors.js
Normal file
7
src/pnet/errors.js
Normal file
@ -0,0 +1,7 @@
|
||||
'use strict'
|
||||
|
||||
module.exports.INVALID_PEER = 'Not a valid peer connection'
|
||||
module.exports.INVALID_PSK = 'Your private shared key is invalid'
|
||||
module.exports.NO_LOCAL_ID = 'No local private key provided'
|
||||
module.exports.NO_HANDSHAKE_CONNECTION = 'No connection for the handshake provided'
|
||||
module.exports.STREAM_ENDED = 'Stream ended prematurely'
|
75
src/pnet/index.js
Normal file
75
src/pnet/index.js
Normal file
@ -0,0 +1,75 @@
|
||||
'use strict'
|
||||
|
||||
const pipe = require('it-pipe')
|
||||
const assert = require('assert')
|
||||
const duplexPair = require('it-pair/duplex')
|
||||
const crypto = require('libp2p-crypto')
|
||||
const Errors = require('./errors')
|
||||
const {
|
||||
createBoxStream,
|
||||
createUnboxStream,
|
||||
decodeV1PSK
|
||||
} = require('./crypto')
|
||||
const handshake = require('it-handshake')
|
||||
const { NONCE_LENGTH } = require('./key-generator')
|
||||
const debug = require('debug')
|
||||
const log = debug('libp2p:pnet')
|
||||
log.err = debug('libp2p:pnet:err')
|
||||
|
||||
/**
|
||||
* Takes a Private Shared Key (psk) and provides a `protect` method
|
||||
* for wrapping existing connections in a private encryption stream
|
||||
*/
|
||||
class Protector {
|
||||
/**
|
||||
* @param {Buffer} keyBuffer The private shared key buffer
|
||||
* @constructor
|
||||
*/
|
||||
constructor (keyBuffer) {
|
||||
const decodedPSK = decodeV1PSK(keyBuffer)
|
||||
this.psk = decodedPSK.psk
|
||||
this.tag = decodedPSK.tag
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a given Connection and creates a private encryption stream
|
||||
* between its two peers from the PSK the Protector instance was
|
||||
* created with.
|
||||
*
|
||||
* @param {Connection} connection The connection to protect
|
||||
* @returns {*} A protected duplex iterable
|
||||
*/
|
||||
async protect (connection) {
|
||||
assert(connection, Errors.NO_HANDSHAKE_CONNECTION)
|
||||
|
||||
// Exchange nonces
|
||||
log('protecting the connection')
|
||||
const localNonce = crypto.randomBytes(NONCE_LENGTH)
|
||||
|
||||
const shake = handshake(connection)
|
||||
shake.write(localNonce)
|
||||
|
||||
const result = await shake.reader.next(NONCE_LENGTH)
|
||||
const remoteNonce = result.value.slice()
|
||||
shake.rest()
|
||||
|
||||
// Create the boxing/unboxing pipe
|
||||
log('exchanged nonces')
|
||||
const [internal, external] = duplexPair()
|
||||
pipe(
|
||||
external,
|
||||
// Encrypt all outbound traffic
|
||||
createBoxStream(localNonce, this.psk),
|
||||
shake.stream,
|
||||
// Decrypt all inbound traffic
|
||||
createUnboxStream(remoteNonce, this.psk),
|
||||
external
|
||||
)
|
||||
|
||||
return internal
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Protector
|
||||
module.exports.errors = Errors
|
||||
module.exports.generate = require('./key-generator')
|
22
src/pnet/key-generator.js
Normal file
22
src/pnet/key-generator.js
Normal file
@ -0,0 +1,22 @@
|
||||
'use strict'
|
||||
|
||||
const crypto = require('crypto')
|
||||
const KEY_LENGTH = 32
|
||||
|
||||
/**
|
||||
* Generates a PSK that can be used in a libp2p-pnet private network
|
||||
* @param {Writer} writer An object containing a `write` method
|
||||
* @returns {void}
|
||||
*/
|
||||
function generate (writer) {
|
||||
const psk = crypto.randomBytes(KEY_LENGTH).toString('hex')
|
||||
writer.write('/key/swarm/psk/1.0.0/\n/base16/\n' + psk)
|
||||
}
|
||||
|
||||
module.exports = generate
|
||||
module.exports.NONCE_LENGTH = 24
|
||||
module.exports.KEY_LENGTH = KEY_LENGTH
|
||||
|
||||
if (require.main === module) {
|
||||
generate(process.stdout)
|
||||
}
|
132
src/pubsub.js
132
src/pubsub.js
@ -1,100 +1,104 @@
|
||||
'use strict'
|
||||
|
||||
const nextTick = require('async/nextTick')
|
||||
const { messages, codes } = require('./errors')
|
||||
const FloodSub = require('libp2p-floodsub')
|
||||
|
||||
const errCode = require('err-code')
|
||||
const { messages, codes } = require('./errors')
|
||||
|
||||
module.exports = (node) => {
|
||||
const floodSub = new FloodSub(node)
|
||||
|
||||
node._floodSub = floodSub
|
||||
module.exports = (node, Pubsub, config) => {
|
||||
const pubsub = new Pubsub(node.peerInfo, node.registrar, config)
|
||||
|
||||
return {
|
||||
subscribe: (topic, options, handler, callback) => {
|
||||
if (typeof options === 'function') {
|
||||
callback = handler
|
||||
handler = options
|
||||
options = {}
|
||||
/**
|
||||
* Subscribe the given handler to a pubsub topic
|
||||
* @param {string} topic
|
||||
* @param {function} handler The handler to subscribe
|
||||
* @returns {void}
|
||||
*/
|
||||
subscribe: (topic, handler) => {
|
||||
if (!node.isStarted() && !pubsub.started) {
|
||||
throw errCode(new Error(messages.NOT_STARTED_YET), codes.PUBSUB_NOT_STARTED)
|
||||
}
|
||||
|
||||
if (!node.isStarted() && !floodSub.started) {
|
||||
return nextTick(callback, errCode(new Error(messages.NOT_STARTED_YET), codes.PUBSUB_NOT_STARTED))
|
||||
if (pubsub.listenerCount(topic) === 0) {
|
||||
pubsub.subscribe(topic)
|
||||
}
|
||||
|
||||
function subscribe (cb) {
|
||||
if (floodSub.listenerCount(topic) === 0) {
|
||||
floodSub.subscribe(topic)
|
||||
}
|
||||
|
||||
floodSub.on(topic, handler)
|
||||
nextTick(cb)
|
||||
}
|
||||
|
||||
subscribe(callback)
|
||||
pubsub.on(topic, handler)
|
||||
},
|
||||
|
||||
unsubscribe: (topic, handler, callback) => {
|
||||
if (!node.isStarted() && !floodSub.started) {
|
||||
return nextTick(callback, errCode(new Error(messages.NOT_STARTED_YET), codes.PUBSUB_NOT_STARTED))
|
||||
/**
|
||||
* Unsubscribes from a pubsub topic
|
||||
* @param {string} topic
|
||||
* @param {function} [handler] The handler to unsubscribe from
|
||||
*/
|
||||
unsubscribe: (topic, handler) => {
|
||||
if (!node.isStarted() && !pubsub.started) {
|
||||
throw errCode(new Error(messages.NOT_STARTED_YET), codes.PUBSUB_NOT_STARTED)
|
||||
}
|
||||
if (!handler && !callback) {
|
||||
floodSub.removeAllListeners(topic)
|
||||
|
||||
if (!handler) {
|
||||
pubsub.removeAllListeners(topic)
|
||||
} else {
|
||||
floodSub.removeListener(topic, handler)
|
||||
pubsub.removeListener(topic, handler)
|
||||
}
|
||||
|
||||
if (floodSub.listenerCount(topic) === 0) {
|
||||
floodSub.unsubscribe(topic)
|
||||
}
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
nextTick(() => callback())
|
||||
if (pubsub.listenerCount(topic) === 0) {
|
||||
pubsub.unsubscribe(topic)
|
||||
}
|
||||
},
|
||||
|
||||
publish: (topic, data, callback) => {
|
||||
if (!node.isStarted() && !floodSub.started) {
|
||||
return nextTick(callback, errCode(new Error(messages.NOT_STARTED_YET), codes.PUBSUB_NOT_STARTED))
|
||||
/**
|
||||
* Publish messages to the given topics.
|
||||
* @param {Array<string>|string} topic
|
||||
* @param {Buffer} data
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
publish: (topic, data) => {
|
||||
if (!node.isStarted() && !pubsub.started) {
|
||||
throw errCode(new Error(messages.NOT_STARTED_YET), codes.PUBSUB_NOT_STARTED)
|
||||
}
|
||||
|
||||
if (!Buffer.isBuffer(data)) {
|
||||
return nextTick(callback, errCode(new Error('data must be a Buffer'), 'ERR_DATA_IS_NOT_A_BUFFER'))
|
||||
try {
|
||||
data = Buffer.from(data)
|
||||
} catch (err) {
|
||||
throw errCode(new Error('data must be convertible to a Buffer'), 'ERR_DATA_IS_NOT_VALID')
|
||||
}
|
||||
|
||||
floodSub.publish(topic, data, callback)
|
||||
return pubsub.publish(topic, data)
|
||||
},
|
||||
|
||||
ls: (callback) => {
|
||||
if (!node.isStarted() && !floodSub.started) {
|
||||
return nextTick(callback, errCode(new Error(messages.NOT_STARTED_YET), codes.PUBSUB_NOT_STARTED))
|
||||
/**
|
||||
* Get a list of topics the node is subscribed to.
|
||||
* @returns {Array<String>} topics
|
||||
*/
|
||||
getTopics: () => {
|
||||
if (!node.isStarted() && !pubsub.started) {
|
||||
throw errCode(new Error(messages.NOT_STARTED_YET), codes.PUBSUB_NOT_STARTED)
|
||||
}
|
||||
|
||||
const subscriptions = Array.from(floodSub.subscriptions)
|
||||
|
||||
nextTick(() => callback(null, subscriptions))
|
||||
return pubsub.getTopics()
|
||||
},
|
||||
|
||||
peers: (topic, callback) => {
|
||||
if (!node.isStarted() && !floodSub.started) {
|
||||
return nextTick(callback, errCode(new Error(messages.NOT_STARTED_YET), codes.PUBSUB_NOT_STARTED))
|
||||
/**
|
||||
* Get a list of the peer-ids that are subscribed to one topic.
|
||||
* @param {string} topic
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
getSubscribers: (topic) => {
|
||||
if (!node.isStarted() && !pubsub.started) {
|
||||
throw errCode(new Error(messages.NOT_STARTED_YET), codes.PUBSUB_NOT_STARTED)
|
||||
}
|
||||
|
||||
if (typeof topic === 'function') {
|
||||
callback = topic
|
||||
topic = null
|
||||
}
|
||||
|
||||
const peers = Array.from(floodSub.peers.values())
|
||||
.filter((peer) => topic ? peer.topics.has(topic) : true)
|
||||
.map((peer) => peer.info.id.toB58String())
|
||||
|
||||
nextTick(() => callback(null, peers))
|
||||
return pubsub.getSubscribers(topic)
|
||||
},
|
||||
|
||||
setMaxListeners (n) {
|
||||
return floodSub.setMaxListeners(n)
|
||||
}
|
||||
return pubsub.setMaxListeners(n)
|
||||
},
|
||||
|
||||
_pubsub: pubsub,
|
||||
|
||||
start: () => pubsub.start(),
|
||||
|
||||
stop: () => pubsub.stop()
|
||||
}
|
||||
}
|
||||
|
160
src/registrar.js
Normal file
160
src/registrar.js
Normal file
@ -0,0 +1,160 @@
|
||||
'use strict'
|
||||
|
||||
const assert = require('assert')
|
||||
const debug = require('debug')
|
||||
const log = debug('libp2p:peer-store')
|
||||
log.error = debug('libp2p:peer-store:error')
|
||||
|
||||
const Topology = require('libp2p-interfaces/src/topology')
|
||||
const { Connection } = require('libp2p-interfaces/src/connection')
|
||||
const PeerInfo = require('peer-info')
|
||||
|
||||
/**
|
||||
* Responsible for notifying registered protocols of events in the network.
|
||||
*/
|
||||
class Registrar {
|
||||
/**
|
||||
* @param {Object} props
|
||||
* @param {PeerStore} props.peerStore
|
||||
* @constructor
|
||||
*/
|
||||
constructor ({ peerStore }) {
|
||||
this.peerStore = peerStore
|
||||
|
||||
/**
|
||||
* Map of connections per peer
|
||||
* TODO: this should be handled by connectionManager
|
||||
* @type {Map<string, Array<conn>>}
|
||||
*/
|
||||
this.connections = new Map()
|
||||
|
||||
/**
|
||||
* Map of topologies
|
||||
*
|
||||
* @type {Map<string, object>}
|
||||
*/
|
||||
this.topologies = new Map()
|
||||
|
||||
this._handle = undefined
|
||||
}
|
||||
|
||||
get handle () {
|
||||
return this._handle
|
||||
}
|
||||
|
||||
set handle (handle) {
|
||||
this._handle = handle
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the registrar
|
||||
* @async
|
||||
*/
|
||||
async close () {
|
||||
// Close all connections we're tracking
|
||||
const tasks = []
|
||||
for (const connectionList of this.connections.values()) {
|
||||
for (const connection of connectionList) {
|
||||
tasks.push(connection.close())
|
||||
}
|
||||
}
|
||||
|
||||
await tasks
|
||||
this.connections.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new connected peer to the record
|
||||
* TODO: this should live in the ConnectionManager
|
||||
* @param {PeerInfo} peerInfo
|
||||
* @param {Connection} conn
|
||||
* @returns {void}
|
||||
*/
|
||||
onConnect (peerInfo, conn) {
|
||||
assert(PeerInfo.isPeerInfo(peerInfo), 'peerInfo must be an instance of peer-info')
|
||||
assert(Connection.isConnection(conn), 'conn must be an instance of interface-connection')
|
||||
|
||||
const id = peerInfo.id.toString()
|
||||
const storedConn = this.connections.get(id)
|
||||
|
||||
if (storedConn) {
|
||||
storedConn.push(conn)
|
||||
} else {
|
||||
this.connections.set(id, [conn])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a disconnected peer from the record
|
||||
* TODO: this should live in the ConnectionManager
|
||||
* @param {PeerInfo} peerInfo
|
||||
* @param {Connection} connection
|
||||
* @param {Error} [error]
|
||||
* @returns {void}
|
||||
*/
|
||||
onDisconnect (peerInfo, connection, error) {
|
||||
assert(PeerInfo.isPeerInfo(peerInfo), 'peerInfo must be an instance of peer-info')
|
||||
|
||||
const id = peerInfo.id.toString()
|
||||
let storedConn = this.connections.get(id)
|
||||
|
||||
if (storedConn && storedConn.length > 1) {
|
||||
storedConn = storedConn.filter((conn) => conn.id === connection.id)
|
||||
this.connections.set(id, storedConn)
|
||||
} else if (storedConn) {
|
||||
for (const [, topology] of this.topologies) {
|
||||
topology.disconnect(peerInfo, error)
|
||||
}
|
||||
|
||||
this.connections.delete(peerInfo.id.toString())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a connection with a peer.
|
||||
* @param {PeerInfo} peerInfo
|
||||
* @returns {Connection}
|
||||
*/
|
||||
getConnection (peerInfo) {
|
||||
assert(PeerInfo.isPeerInfo(peerInfo), 'peerInfo must be an instance of peer-info')
|
||||
|
||||
const connections = this.connections.get(peerInfo.id.toString())
|
||||
// Return the first, open connection
|
||||
if (connections) {
|
||||
return connections.find(connection => connection.stat.status === 'open')
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Register handlers for a set of multicodecs given
|
||||
* @param {Topology} topology protocol topology
|
||||
* @return {string} registrar identifier
|
||||
*/
|
||||
register (topology) {
|
||||
assert(
|
||||
Topology.isTopology(topology),
|
||||
'topology must be an instance of interfaces/topology')
|
||||
|
||||
// Create topology
|
||||
const id = (parseInt(Math.random() * 1e9)).toString(36) + Date.now()
|
||||
|
||||
this.topologies.set(id, topology)
|
||||
|
||||
// Set registrar
|
||||
topology.registrar = this
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister topology.
|
||||
* @param {string} id registrar identifier
|
||||
* @return {boolean} unregistered successfully
|
||||
*/
|
||||
unregister (id) {
|
||||
return this.topologies.delete(id)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Registrar
|
212
src/transport-manager.js
Normal file
212
src/transport-manager.js
Normal file
@ -0,0 +1,212 @@
|
||||
'use strict'
|
||||
|
||||
const pSettle = require('p-settle')
|
||||
const { codes } = require('./errors')
|
||||
const errCode = require('err-code')
|
||||
const debug = require('debug')
|
||||
const log = debug('libp2p:transports')
|
||||
log.error = debug('libp2p:transports:error')
|
||||
|
||||
class TransportManager {
|
||||
/**
|
||||
* @constructor
|
||||
* @param {object} options
|
||||
* @param {Libp2p} options.libp2p The Libp2p instance. It will be passed to the transports.
|
||||
* @param {Upgrader} options.upgrader The upgrader to provide to the transports
|
||||
*/
|
||||
constructor ({ libp2p, upgrader }) {
|
||||
this.libp2p = libp2p
|
||||
this.upgrader = upgrader
|
||||
this._transports = new Map()
|
||||
this._listeners = new Map()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a `Transport` to the manager
|
||||
*
|
||||
* @param {String} key
|
||||
* @param {Transport} Transport
|
||||
* @returns {void}
|
||||
*/
|
||||
add (key, Transport) {
|
||||
log('adding %s', key)
|
||||
if (!key) {
|
||||
throw errCode(new Error(`Transport must have a valid key, was given '${key}'`), codes.ERR_INVALID_KEY)
|
||||
}
|
||||
if (this._transports.has(key)) {
|
||||
throw errCode(new Error('There is already a transport with this key'), codes.ERR_DUPLICATE_TRANSPORT)
|
||||
}
|
||||
|
||||
const transport = new Transport({
|
||||
libp2p: this.libp2p,
|
||||
upgrader: this.upgrader
|
||||
})
|
||||
|
||||
this._transports.set(key, transport)
|
||||
if (!this._listeners.has(key)) {
|
||||
this._listeners.set(key, [])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops all listeners
|
||||
* @async
|
||||
*/
|
||||
async close () {
|
||||
const tasks = []
|
||||
for (const [key, listeners] of this._listeners) {
|
||||
log('closing listeners for %s', key)
|
||||
while (listeners.length) {
|
||||
const listener = listeners.pop()
|
||||
tasks.push(listener.close())
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(tasks)
|
||||
log('all listeners closed')
|
||||
for (const key of this._listeners.keys()) {
|
||||
this._listeners.set(key, [])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dials the given Multiaddr over it's supported transport
|
||||
* @param {Multiaddr} ma
|
||||
* @param {*} options
|
||||
* @returns {Promise<Connection>}
|
||||
*/
|
||||
async dial (ma, options) {
|
||||
const transport = this.transportForMultiaddr(ma)
|
||||
if (!transport) {
|
||||
throw errCode(new Error(`No transport available for address ${String(ma)}`), codes.ERR_TRANSPORT_UNAVAILABLE)
|
||||
}
|
||||
|
||||
try {
|
||||
return await transport.dial(ma, options)
|
||||
} catch (err) {
|
||||
if (err.code) throw err
|
||||
throw errCode(err, codes.ERR_TRANSPORT_DIAL_FAILED)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all Multiaddr's the listeners are using
|
||||
* @returns {Multiaddr[]}
|
||||
*/
|
||||
getAddrs () {
|
||||
let addrs = []
|
||||
for (const listeners of this._listeners.values()) {
|
||||
for (const listener of listeners) {
|
||||
addrs = [...addrs, ...listener.getAddrs()]
|
||||
}
|
||||
}
|
||||
return addrs
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the transports instances.
|
||||
* @returns {Iterator<Transport>}
|
||||
*/
|
||||
getTransports () {
|
||||
return this._transports.values()
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a transport that matches the given Multiaddr
|
||||
* @param {Multiaddr} ma
|
||||
* @returns {Transport|null}
|
||||
*/
|
||||
transportForMultiaddr (ma) {
|
||||
for (const transport of this._transports.values()) {
|
||||
const addrs = transport.filter([ma])
|
||||
if (addrs.length) return transport
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts listeners for each given Multiaddr.
|
||||
* @async
|
||||
* @param {Multiaddr[]} addrs
|
||||
*/
|
||||
async listen (addrs) {
|
||||
if (addrs.length === 0) {
|
||||
log('no addresses were provided for listening, this node is dial only')
|
||||
return
|
||||
}
|
||||
|
||||
const couldNotListen = []
|
||||
for (const [key, transport] of this._transports.entries()) {
|
||||
const supportedAddrs = transport.filter(addrs)
|
||||
const tasks = []
|
||||
|
||||
// For each supported multiaddr, create a listener
|
||||
for (const addr of supportedAddrs) {
|
||||
log('creating listener for %s on %s', key, addr)
|
||||
const listener = transport.createListener({}, this.onConnection)
|
||||
this._listeners.get(key).push(listener)
|
||||
|
||||
// We need to attempt to listen on everything
|
||||
tasks.push(listener.listen(addr))
|
||||
}
|
||||
|
||||
// Keep track of transports we had no addresses for
|
||||
if (tasks.length === 0) {
|
||||
couldNotListen.push(key)
|
||||
continue
|
||||
}
|
||||
|
||||
const results = await pSettle(tasks)
|
||||
// If we are listening on at least 1 address, succeed.
|
||||
// TODO: we should look at adding a retry (`p-retry`) here to better support
|
||||
// listening on remote addresses as they may be offline. We could then potentially
|
||||
// just wait for any (`p-any`) listener to succeed on each transport before returning
|
||||
const isListening = results.find(r => r.isFulfilled === true)
|
||||
if (!isListening) {
|
||||
throw errCode(new Error(`Transport (${key}) could not listen on any available address`), codes.ERR_NO_VALID_ADDRESSES)
|
||||
}
|
||||
}
|
||||
|
||||
// If no transports were able to listen, throw an error. This likely
|
||||
// means we were given addresses we do not have transports for
|
||||
if (couldNotListen.length === this._transports.size) {
|
||||
throw errCode(new Error(`no valid addresses were provided for transports [${couldNotListen}]`), codes.ERR_NO_VALID_ADDRESSES)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given transport from the manager.
|
||||
* If a transport has any running listeners, they will be closed.
|
||||
*
|
||||
* @async
|
||||
* @param {string} key
|
||||
*/
|
||||
async remove (key) {
|
||||
log('removing %s', key)
|
||||
if (this._listeners.has(key)) {
|
||||
// Close any running listeners
|
||||
for (const listener of this._listeners.get(key)) {
|
||||
await listener.close()
|
||||
}
|
||||
}
|
||||
|
||||
this._transports.delete(key)
|
||||
this._listeners.delete(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all transports from the manager.
|
||||
* If any listeners are running, they will be closed.
|
||||
* @async
|
||||
*/
|
||||
async removeAll () {
|
||||
const tasks = []
|
||||
for (const key of this._transports.keys()) {
|
||||
tasks.push(this.remove(key))
|
||||
}
|
||||
|
||||
await Promise.all(tasks)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TransportManager
|
401
src/upgrader.js
Normal file
401
src/upgrader.js
Normal file
@ -0,0 +1,401 @@
|
||||
'use strict'
|
||||
|
||||
const debug = require('debug')
|
||||
const log = debug('libp2p:upgrader')
|
||||
log.error = debug('libp2p:upgrader:error')
|
||||
const Multistream = require('multistream-select')
|
||||
const { Connection } = require('libp2p-interfaces/src/connection')
|
||||
const PeerId = require('peer-id')
|
||||
const pipe = require('it-pipe')
|
||||
const errCode = require('err-code')
|
||||
const mutableProxy = require('mutable-proxy')
|
||||
|
||||
const { codes } = require('./errors')
|
||||
|
||||
/**
|
||||
* @typedef MultiaddrConnection
|
||||
* @property {function} sink
|
||||
* @property {AsyncIterator} source
|
||||
* @property {*} conn
|
||||
* @property {Multiaddr} remoteAddr
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef CryptoResult
|
||||
* @property {*} conn A duplex iterable
|
||||
* @property {PeerId} remotePeer
|
||||
* @property {string} protocol
|
||||
*/
|
||||
|
||||
class Upgrader {
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {PeerId} options.localPeer
|
||||
* @param {Metrics} options.metrics
|
||||
* @param {Map<string, Crypto>} options.cryptos
|
||||
* @param {Map<string, Muxer>} options.muxers
|
||||
* @param {function(Connection)} options.onConnection Called when a connection is upgraded
|
||||
* @param {function(Connection)} options.onConnectionEnd
|
||||
*/
|
||||
constructor ({
|
||||
localPeer,
|
||||
metrics,
|
||||
cryptos,
|
||||
muxers,
|
||||
onConnectionEnd = () => {},
|
||||
onConnection = () => {}
|
||||
}) {
|
||||
this.localPeer = localPeer
|
||||
this.metrics = metrics
|
||||
this.cryptos = cryptos || new Map()
|
||||
this.muxers = muxers || new Map()
|
||||
this.protector = null
|
||||
this.protocols = new Map()
|
||||
this.onConnection = onConnection
|
||||
this.onConnectionEnd = onConnectionEnd
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrades an inbound connection
|
||||
* @async
|
||||
* @param {MultiaddrConnection} maConn
|
||||
* @returns {Promise<Connection>}
|
||||
*/
|
||||
async upgradeInbound (maConn) {
|
||||
let encryptedConn
|
||||
let remotePeer
|
||||
let muxedConnection
|
||||
let Muxer
|
||||
let cryptoProtocol
|
||||
let setPeer
|
||||
let proxyPeer
|
||||
|
||||
if (this.metrics) {
|
||||
({ setTarget: setPeer, proxy: proxyPeer } = mutableProxy())
|
||||
const idString = (parseInt(Math.random() * 1e9)).toString(36) + Date.now()
|
||||
setPeer({ toString: () => idString })
|
||||
maConn = this.metrics.trackStream({ stream: maConn, remotePeer: proxyPeer })
|
||||
}
|
||||
|
||||
log('Starting the inbound connection upgrade')
|
||||
|
||||
// Protect
|
||||
let protectedConn = maConn
|
||||
if (this.protector) {
|
||||
protectedConn = await this.protector.protect(maConn)
|
||||
}
|
||||
|
||||
try {
|
||||
// Encrypt the connection
|
||||
({
|
||||
conn: encryptedConn,
|
||||
remotePeer,
|
||||
protocol: cryptoProtocol
|
||||
} = await this._encryptInbound(this.localPeer, protectedConn, this.cryptos))
|
||||
|
||||
// Multiplex the connection
|
||||
;({ stream: muxedConnection, Muxer } = await this._multiplexInbound(encryptedConn, this.muxers))
|
||||
} catch (err) {
|
||||
log.error('Failed to upgrade inbound connection', err)
|
||||
await maConn.close(err)
|
||||
throw err
|
||||
}
|
||||
|
||||
if (this.metrics) {
|
||||
this.metrics.updatePlaceholder(proxyPeer, remotePeer)
|
||||
setPeer(remotePeer)
|
||||
}
|
||||
|
||||
log('Successfully upgraded inbound connection')
|
||||
|
||||
return this._createConnection({
|
||||
cryptoProtocol,
|
||||
direction: 'inbound',
|
||||
maConn,
|
||||
muxedConnection,
|
||||
Muxer,
|
||||
remotePeer
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrades an outbound connection
|
||||
* @async
|
||||
* @param {MultiaddrConnection} maConn
|
||||
* @returns {Promise<Connection>}
|
||||
*/
|
||||
async upgradeOutbound (maConn) {
|
||||
let remotePeerId
|
||||
try {
|
||||
remotePeerId = PeerId.createFromB58String(maConn.remoteAddr.getPeerId())
|
||||
} catch (err) {
|
||||
log.error('multiaddr did not contain a valid peer id', err)
|
||||
}
|
||||
|
||||
let encryptedConn
|
||||
let remotePeer
|
||||
let muxedConnection
|
||||
let cryptoProtocol
|
||||
let Muxer
|
||||
let setPeer
|
||||
let proxyPeer
|
||||
|
||||
if (this.metrics) {
|
||||
({ setTarget: setPeer, proxy: proxyPeer } = mutableProxy())
|
||||
const idString = (parseInt(Math.random() * 1e9)).toString(36) + Date.now()
|
||||
setPeer({ toString: () => idString })
|
||||
maConn = this.metrics.trackStream({ stream: maConn, remotePeer: proxyPeer })
|
||||
}
|
||||
|
||||
log('Starting the outbound connection upgrade')
|
||||
|
||||
// Protect
|
||||
let protectedConn = maConn
|
||||
if (this.protector) {
|
||||
protectedConn = await this.protector.protect(maConn)
|
||||
}
|
||||
|
||||
try {
|
||||
// Encrypt the connection
|
||||
({
|
||||
conn: encryptedConn,
|
||||
remotePeer,
|
||||
protocol: cryptoProtocol
|
||||
} = await this._encryptOutbound(this.localPeer, protectedConn, remotePeerId, this.cryptos))
|
||||
|
||||
// Multiplex the connection
|
||||
;({ stream: muxedConnection, Muxer } = await this._multiplexOutbound(encryptedConn, this.muxers))
|
||||
} catch (err) {
|
||||
log.error('Failed to upgrade outbound connection', err)
|
||||
await maConn.close(err)
|
||||
throw err
|
||||
}
|
||||
|
||||
if (this.metrics) {
|
||||
this.metrics.updatePlaceholder(proxyPeer, remotePeer)
|
||||
setPeer(remotePeer)
|
||||
}
|
||||
|
||||
log('Successfully upgraded outbound connection')
|
||||
|
||||
return this._createConnection({
|
||||
cryptoProtocol,
|
||||
direction: 'outbound',
|
||||
maConn,
|
||||
muxedConnection,
|
||||
Muxer,
|
||||
remotePeer
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenience method for generating a new `Connection`
|
||||
* @private
|
||||
* @param {object} options
|
||||
* @param {string} cryptoProtocol The crypto protocol that was negotiated
|
||||
* @param {string} direction One of ['inbound', 'outbound']
|
||||
* @param {MultiaddrConnection} maConn The transport layer connection
|
||||
* @param {*} muxedConnection A duplex connection returned from multiplexer selection
|
||||
* @param {Muxer} Muxer The muxer to be used for muxing
|
||||
* @param {PeerId} remotePeer The peer the connection is with
|
||||
* @returns {Connection}
|
||||
*/
|
||||
_createConnection ({
|
||||
cryptoProtocol,
|
||||
direction,
|
||||
maConn,
|
||||
muxedConnection,
|
||||
Muxer,
|
||||
remotePeer
|
||||
}) {
|
||||
// Create the muxer
|
||||
const muxer = new Muxer({
|
||||
// Run anytime a remote stream is created
|
||||
onStream: async muxedStream => {
|
||||
const mss = new Multistream.Listener(muxedStream)
|
||||
try {
|
||||
const { stream, protocol } = await mss.handle(Array.from(this.protocols.keys()))
|
||||
log('%s: incoming stream opened on %s', direction, protocol)
|
||||
if (this.metrics) this.metrics.trackStream({ stream, remotePeer, protocol })
|
||||
connection.addStream(stream, protocol)
|
||||
this._onStream({ connection, stream, protocol })
|
||||
} catch (err) {
|
||||
log.error(err)
|
||||
}
|
||||
},
|
||||
// Run anytime a stream closes
|
||||
onStreamEnd: muxedStream => {
|
||||
connection.removeStream(muxedStream.id)
|
||||
}
|
||||
})
|
||||
|
||||
const newStream = async protocols => {
|
||||
log('%s: starting new stream on %s', direction, protocols)
|
||||
const muxedStream = muxer.newStream()
|
||||
const mss = new Multistream.Dialer(muxedStream)
|
||||
try {
|
||||
const { stream, protocol } = await mss.select(protocols)
|
||||
if (this.metrics) this.metrics.trackStream({ stream, remotePeer, protocol })
|
||||
return { stream: { ...muxedStream, ...stream }, protocol }
|
||||
} catch (err) {
|
||||
log.error('could not create new stream', err)
|
||||
throw errCode(err, codes.ERR_UNSUPPORTED_PROTOCOL)
|
||||
}
|
||||
}
|
||||
|
||||
// Pipe all data through the muxer
|
||||
pipe(muxedConnection, muxer, muxedConnection)
|
||||
|
||||
maConn.timeline.upgraded = Date.now()
|
||||
const _timeline = maConn.timeline
|
||||
maConn.timeline = new Proxy(_timeline, {
|
||||
set: (...args) => {
|
||||
if (args[1] === 'close' && args[2] && !_timeline.close) {
|
||||
connection.stat.status = 'closed'
|
||||
this.onConnectionEnd(connection)
|
||||
}
|
||||
|
||||
return Reflect.set(...args)
|
||||
}
|
||||
})
|
||||
|
||||
// Create the connection
|
||||
const connection = new Connection({
|
||||
localAddr: maConn.localAddr,
|
||||
remoteAddr: maConn.remoteAddr,
|
||||
localPeer: this.localPeer,
|
||||
remotePeer: remotePeer,
|
||||
stat: {
|
||||
direction,
|
||||
timeline: maConn.timeline,
|
||||
multiplexer: Muxer.multicodec,
|
||||
encryption: cryptoProtocol
|
||||
},
|
||||
newStream,
|
||||
getStreams: () => muxer.streams,
|
||||
close: err => maConn.close(err)
|
||||
})
|
||||
|
||||
this.onConnection(connection)
|
||||
|
||||
return connection
|
||||
}
|
||||
|
||||
/**
|
||||
* Routes incoming streams to the correct handler
|
||||
* @private
|
||||
* @param {object} options
|
||||
* @param {Connection} options.connection The connection the stream belongs to
|
||||
* @param {Stream} options.stream
|
||||
* @param {string} options.protocol
|
||||
*/
|
||||
_onStream ({ connection, stream, protocol }) {
|
||||
const handler = this.protocols.get(protocol)
|
||||
handler({ connection, stream, protocol })
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to encrypt the incoming `connection` with the provided `cryptos`.
|
||||
* @private
|
||||
* @async
|
||||
* @param {PeerId} localPeer The initiators PeerInfo
|
||||
* @param {*} connection
|
||||
* @param {Map<string, Crypto>} cryptos
|
||||
* @returns {CryptoResult} An encrypted connection, remote peer `PeerId` and the protocol of the `Crypto` used
|
||||
*/
|
||||
async _encryptInbound (localPeer, connection, cryptos) {
|
||||
const mss = new Multistream.Listener(connection)
|
||||
const protocols = Array.from(cryptos.keys())
|
||||
log('handling inbound crypto protocol selection', protocols)
|
||||
|
||||
try {
|
||||
const { stream, protocol } = await mss.handle(protocols)
|
||||
const crypto = cryptos.get(protocol)
|
||||
log('encrypting inbound connection...')
|
||||
|
||||
return {
|
||||
...await crypto.secureInbound(localPeer, stream),
|
||||
protocol
|
||||
}
|
||||
} catch (err) {
|
||||
throw errCode(err, codes.ERR_ENCRYPTION_FAILED)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to encrypt the given `connection` with the provided `cryptos`.
|
||||
* The first `Crypto` module to succeed will be used
|
||||
* @private
|
||||
* @async
|
||||
* @param {PeerId} localPeer The initiators PeerInfo
|
||||
* @param {*} connection
|
||||
* @param {PeerId} remotePeerId
|
||||
* @param {Map<string, Crypto>} cryptos
|
||||
* @returns {CryptoResult} An encrypted connection, remote peer `PeerId` and the protocol of the `Crypto` used
|
||||
*/
|
||||
async _encryptOutbound (localPeer, connection, remotePeerId, cryptos) {
|
||||
const mss = new Multistream.Dialer(connection)
|
||||
const protocols = Array.from(cryptos.keys())
|
||||
log('selecting outbound crypto protocol', protocols)
|
||||
|
||||
try {
|
||||
const { stream, protocol } = await mss.select(protocols)
|
||||
const crypto = cryptos.get(protocol)
|
||||
log('encrypting outbound connection to %j', remotePeerId)
|
||||
|
||||
return {
|
||||
...await crypto.secureOutbound(localPeer, stream, remotePeerId),
|
||||
protocol
|
||||
}
|
||||
} catch (err) {
|
||||
throw errCode(err, codes.ERR_ENCRYPTION_FAILED)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects one of the given muxers via multistream-select. That
|
||||
* muxer will be used for all future streams on the connection.
|
||||
* @private
|
||||
* @async
|
||||
* @param {*} connection A basic duplex connection to multiplex
|
||||
* @param {Map<string, Muxer>} muxers The muxers to attempt multiplexing with
|
||||
* @returns {*} A muxed connection
|
||||
*/
|
||||
async _multiplexOutbound (connection, muxers) {
|
||||
const dialer = new Multistream.Dialer(connection)
|
||||
const protocols = Array.from(muxers.keys())
|
||||
log('outbound selecting muxer %s', protocols)
|
||||
try {
|
||||
const { stream, protocol } = await dialer.select(protocols)
|
||||
log('%s selected as muxer protocol', protocol)
|
||||
const Muxer = muxers.get(protocol)
|
||||
return { stream, Muxer }
|
||||
} catch (err) {
|
||||
throw errCode(err, codes.ERR_MUXER_UNAVAILABLE)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers support for one of the given muxers via multistream-select. The
|
||||
* selected muxer will be used for all future streams on the connection.
|
||||
* @private
|
||||
* @async
|
||||
* @param {*} connection A basic duplex connection to multiplex
|
||||
* @param {Map<string, Muxer>} muxers The muxers to attempt multiplexing with
|
||||
* @returns {*} A muxed connection
|
||||
*/
|
||||
async _multiplexInbound (connection, muxers) {
|
||||
const listener = new Multistream.Listener(connection)
|
||||
const protocols = Array.from(muxers.keys())
|
||||
log('inbound handling muxers %s', protocols)
|
||||
try {
|
||||
const { stream, protocol } = await listener.handle(protocols)
|
||||
const Muxer = muxers.get(protocol)
|
||||
return { stream, Muxer }
|
||||
} catch (err) {
|
||||
throw errCode(err, codes.ERR_MUXER_UNAVAILABLE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Upgrader
|
@ -1,33 +0,0 @@
|
||||
'use strict'
|
||||
const once = require('once')
|
||||
|
||||
/**
|
||||
* Registers `handler` to each event in `events`. The `handler`
|
||||
* will only be called for the first event fired, at which point
|
||||
* the `handler` will be removed as a listener.
|
||||
*
|
||||
* Ensures `handler` is only called once.
|
||||
*
|
||||
* @example
|
||||
* // will call `callback` when `start` or `error` is emitted by `this`
|
||||
* emitFirst(this, ['error', 'start'], callback)
|
||||
*
|
||||
* @private
|
||||
* @param {EventEmitter} emitter The emitter to listen on
|
||||
* @param {Array<string>} events The events to listen for
|
||||
* @param {function(*)} handler The handler to call when an event is triggered
|
||||
* @returns {void}
|
||||
*/
|
||||
function emitFirst (emitter, events, handler) {
|
||||
handler = once(handler)
|
||||
events.forEach((e) => {
|
||||
emitter.once(e, (...args) => {
|
||||
events.forEach((ev) => {
|
||||
emitter.removeListener(ev, handler)
|
||||
})
|
||||
handler.apply(emitter, args)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports.emitFirst = emitFirst
|
@ -1,4 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
require('./circuit-relay.browser')
|
||||
require('./transports.browser')
|
@ -1,98 +0,0 @@
|
||||
/* eslint-env mocha */
|
||||
'use strict'
|
||||
|
||||
const chai = require('chai')
|
||||
chai.use(require('dirty-chai'))
|
||||
const expect = chai.expect
|
||||
|
||||
const createNode = require('./utils/create-node')
|
||||
const tryEcho = require('./utils/try-echo')
|
||||
const echo = require('./utils/echo')
|
||||
|
||||
const {
|
||||
getPeerRelay
|
||||
} = require('./utils/constants')
|
||||
|
||||
function setupNodeWithRelay (addrs, options = {}) {
|
||||
options = {
|
||||
config: {
|
||||
relay: {
|
||||
enabled: true
|
||||
},
|
||||
...options.config
|
||||
},
|
||||
...options
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
createNode(addrs, options, (err, node) => {
|
||||
expect(err).to.not.exist()
|
||||
|
||||
node.handle(echo.multicodec, echo)
|
||||
node.start((err) => {
|
||||
expect(err).to.not.exist()
|
||||
resolve(node)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
describe('circuit relay', () => {
|
||||
let browserNode1
|
||||
let browserNode2
|
||||
let peerRelay
|
||||
|
||||
before('get peer relay', async () => {
|
||||
peerRelay = await new Promise(resolve => {
|
||||
getPeerRelay((err, peer) => {
|
||||
expect(err).to.not.exist()
|
||||
resolve(peer)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
before('create the browser nodes', async () => {
|
||||
[browserNode1, browserNode2] = await Promise.all([
|
||||
setupNodeWithRelay([]),
|
||||
setupNodeWithRelay([])
|
||||
])
|
||||
})
|
||||
|
||||
before('connect to the relay node', async () => {
|
||||
await Promise.all(
|
||||
[browserNode1, browserNode2].map((node) => {
|
||||
return new Promise(resolve => {
|
||||
node.dialProtocol(peerRelay, (err) => {
|
||||
expect(err).to.not.exist()
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
before('give time for HOP support to be determined', async () => {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 1e3)
|
||||
})
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await Promise.all(
|
||||
[browserNode1, browserNode2].map((node) => {
|
||||
return new Promise((resolve) => {
|
||||
node.stop(resolve)
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should be able to echo over relay', (done) => {
|
||||
browserNode1.dialProtocol(browserNode2.peerInfo, echo.multicodec, (err, conn) => {
|
||||
expect(err).to.not.exist()
|
||||
expect(conn).to.exist()
|
||||
|
||||
tryEcho(conn, done)
|
||||
})
|
||||
})
|
||||
})
|
@ -1,215 +0,0 @@
|
||||
/* eslint-env mocha */
|
||||
'use strict'
|
||||
|
||||
const chai = require('chai')
|
||||
chai.use(require('dirty-chai'))
|
||||
const expect = chai.expect
|
||||
const sinon = require('sinon')
|
||||
const waterfall = require('async/waterfall')
|
||||
const series = require('async/series')
|
||||
const parallel = require('async/parallel')
|
||||
const Circuit = require('libp2p-circuit')
|
||||
const multiaddr = require('multiaddr')
|
||||
|
||||
const createNode = require('./utils/create-node')
|
||||
const tryEcho = require('./utils/try-echo')
|
||||
const echo = require('./utils/echo')
|
||||
|
||||
describe('circuit relay', () => {
|
||||
let handlerSpies = []
|
||||
let relayNode1
|
||||
let relayNode2
|
||||
let nodeWS1
|
||||
let nodeWS2
|
||||
let nodeTCP1
|
||||
let nodeTCP2
|
||||
|
||||
function setupNode (addrs, options, callback) {
|
||||
if (typeof options === 'function') {
|
||||
callback = options
|
||||
options = {}
|
||||
}
|
||||
|
||||
options = options || {}
|
||||
|
||||
return createNode(addrs, options, (err, node) => {
|
||||
expect(err).to.not.exist()
|
||||
|
||||
node.handle('/echo/1.0.0', echo)
|
||||
node.start((err) => {
|
||||
expect(err).to.not.exist()
|
||||
|
||||
handlerSpies.push(sinon.spy(
|
||||
node._switch.transports[Circuit.tag].listeners[0].hopHandler, 'handle'
|
||||
))
|
||||
|
||||
callback(node)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
before(function (done) {
|
||||
this.timeout(20 * 1000)
|
||||
|
||||
waterfall([
|
||||
// set up passive relay
|
||||
(cb) => setupNode([
|
||||
'/ip4/0.0.0.0/tcp/0/ws',
|
||||
'/ip4/0.0.0.0/tcp/0'
|
||||
], {
|
||||
config: {
|
||||
relay: {
|
||||
enabled: true,
|
||||
hop: {
|
||||
enabled: true,
|
||||
active: false // passive relay
|
||||
}
|
||||
}
|
||||
}
|
||||
}, (node) => {
|
||||
relayNode1 = node
|
||||
cb()
|
||||
}),
|
||||
// setup active relay
|
||||
(cb) => setupNode([
|
||||
'/ip4/0.0.0.0/tcp/0/ws',
|
||||
'/ip4/0.0.0.0/tcp/0'
|
||||
], {
|
||||
config: {
|
||||
relay: {
|
||||
enabled: true,
|
||||
hop: {
|
||||
enabled: true,
|
||||
active: false // passive relay
|
||||
}
|
||||
}
|
||||
}
|
||||
}, (node) => {
|
||||
relayNode2 = node
|
||||
cb()
|
||||
}),
|
||||
// setup node with WS
|
||||
(cb) => setupNode([
|
||||
'/ip4/0.0.0.0/tcp/0/ws'
|
||||
], {
|
||||
config: {
|
||||
relay: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
}, (node) => {
|
||||
nodeWS1 = node
|
||||
cb()
|
||||
}),
|
||||
// setup node with WS
|
||||
(cb) => setupNode([
|
||||
'/ip4/0.0.0.0/tcp/0/ws'
|
||||
], {
|
||||
config: {
|
||||
relay: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
}, (node) => {
|
||||
nodeWS2 = node
|
||||
cb()
|
||||
}),
|
||||
// set up node with TCP
|
||||
(cb) => setupNode([
|
||||
'/ip4/0.0.0.0/tcp/0'
|
||||
], {
|
||||
config: {
|
||||
relay: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
}, (node) => {
|
||||
nodeTCP1 = node
|
||||
cb()
|
||||
}),
|
||||
// set up node with TCP
|
||||
(cb) => setupNode([
|
||||
'/ip4/0.0.0.0/tcp/0'
|
||||
], {
|
||||
config: {
|
||||
relay: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
}, (node) => {
|
||||
nodeTCP2 = node
|
||||
cb()
|
||||
})
|
||||
], (err) => {
|
||||
expect(err).to.not.exist()
|
||||
|
||||
series([
|
||||
(cb) => nodeWS1.dial(relayNode1.peerInfo, cb),
|
||||
(cb) => nodeWS1.dial(relayNode2.peerInfo, cb),
|
||||
(cb) => nodeTCP1.dial(relayNode1.peerInfo, cb),
|
||||
(cb) => nodeTCP2.dial(relayNode2.peerInfo, cb)
|
||||
], done)
|
||||
})
|
||||
})
|
||||
|
||||
after((done) => {
|
||||
parallel([
|
||||
(cb) => relayNode1.stop(cb),
|
||||
(cb) => relayNode2.stop(cb),
|
||||
(cb) => nodeWS1.stop(cb),
|
||||
(cb) => nodeWS2.stop(cb),
|
||||
(cb) => nodeTCP1.stop(cb),
|
||||
(cb) => nodeTCP2.stop(cb)
|
||||
], done)
|
||||
})
|
||||
|
||||
describe('any relay', function () {
|
||||
this.timeout(20 * 1000)
|
||||
|
||||
it('dial from WS1 to TCP1 over any R', (done) => {
|
||||
nodeWS1.dialProtocol(nodeTCP1.peerInfo, '/echo/1.0.0', (err, conn) => {
|
||||
expect(err).to.not.exist()
|
||||
expect(conn).to.exist()
|
||||
tryEcho(conn, done)
|
||||
})
|
||||
})
|
||||
|
||||
it('fail to dial - no R from WS2 to TCP1', (done) => {
|
||||
nodeWS2.dialProtocol(nodeTCP2.peerInfo, '/echo/1.0.0', (err, conn) => {
|
||||
expect(err).to.exist()
|
||||
expect(conn).to.not.exist()
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('explicit relay', function () {
|
||||
this.timeout(20 * 1000)
|
||||
|
||||
it('dial from WS1 to TCP1 over R1', (done) => {
|
||||
nodeWS1.dialProtocol(nodeTCP1.peerInfo, '/echo/1.0.0', (err, conn) => {
|
||||
expect(err).to.not.exist()
|
||||
expect(conn).to.exist()
|
||||
|
||||
tryEcho(conn, () => {
|
||||
const addr = multiaddr(handlerSpies[0].args[2][0].dstPeer.addrs[0]).toString()
|
||||
expect(addr).to.equal(`/ipfs/${nodeTCP1.peerInfo.id.toB58String()}`)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('dial from WS1 to TCP2 over R2', (done) => {
|
||||
nodeWS1.dialProtocol(nodeTCP2.peerInfo, '/echo/1.0.0', (err, conn) => {
|
||||
expect(err).to.not.exist()
|
||||
expect(conn).to.exist()
|
||||
|
||||
tryEcho(conn, () => {
|
||||
const addr = multiaddr(handlerSpies[1].args[2][0].dstPeer.addrs[0]).toString()
|
||||
expect(addr).to.equal(`/ipfs/${nodeTCP2.peerInfo.id.toB58String()}`)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
@ -1,332 +0,0 @@
|
||||
/* eslint-env mocha */
|
||||
'use strict'
|
||||
|
||||
const chai = require('chai')
|
||||
chai.use(require('dirty-chai'))
|
||||
const expect = chai.expect
|
||||
const PeerInfo = require('peer-info')
|
||||
const PeerId = require('peer-id')
|
||||
const waterfall = require('async/waterfall')
|
||||
const WS = require('libp2p-websockets')
|
||||
const Bootstrap = require('libp2p-bootstrap')
|
||||
const DelegatedPeerRouter = require('libp2p-delegated-peer-routing')
|
||||
const DelegatedContentRouter = require('libp2p-delegated-content-routing')
|
||||
const DHT = require('libp2p-kad-dht')
|
||||
|
||||
const validateConfig = require('../src/config').validate
|
||||
|
||||
describe('configuration', () => {
|
||||
let peerInfo
|
||||
|
||||
before((done) => {
|
||||
waterfall([
|
||||
(cb) => PeerId.create({ bits: 512 }, cb),
|
||||
(peerId, cb) => PeerInfo.create(peerId, cb),
|
||||
(info, cb) => {
|
||||
peerInfo = info
|
||||
cb()
|
||||
}
|
||||
], () => done())
|
||||
})
|
||||
|
||||
it('should throw an error if peerInfo is missing', () => {
|
||||
expect(() => {
|
||||
validateConfig({
|
||||
modules: {
|
||||
transport: [ WS ]
|
||||
}
|
||||
})
|
||||
}).to.throw()
|
||||
})
|
||||
|
||||
it('should throw an error if modules is missing', () => {
|
||||
expect(() => {
|
||||
validateConfig({
|
||||
peerInfo
|
||||
})
|
||||
}).to.throw()
|
||||
})
|
||||
|
||||
it('should throw an error if there are no transports', () => {
|
||||
expect(() => {
|
||||
validateConfig({
|
||||
peerInfo,
|
||||
modules: {
|
||||
transport: [ ]
|
||||
}
|
||||
})
|
||||
}).to.throw('ERROR_EMPTY')
|
||||
})
|
||||
|
||||
it('should add defaults to config', () => {
|
||||
const options = {
|
||||
peerInfo,
|
||||
modules: {
|
||||
transport: [ WS ],
|
||||
peerDiscovery: [ Bootstrap ],
|
||||
dht: DHT
|
||||
}
|
||||
}
|
||||
|
||||
const expected = {
|
||||
peerInfo,
|
||||
connectionManager: {
|
||||
minPeers: 25
|
||||
},
|
||||
modules: {
|
||||
transport: [ WS ],
|
||||
peerDiscovery: [ Bootstrap ],
|
||||
dht: DHT
|
||||
},
|
||||
config: {
|
||||
peerDiscovery: {
|
||||
autoDial: true
|
||||
},
|
||||
EXPERIMENTAL: {
|
||||
pubsub: false
|
||||
},
|
||||
dht: {
|
||||
kBucketSize: 20,
|
||||
enabled: false,
|
||||
randomWalk: {
|
||||
enabled: false,
|
||||
queriesPerPeriod: 1,
|
||||
interval: 300000,
|
||||
timeout: 10000
|
||||
}
|
||||
},
|
||||
relay: {
|
||||
enabled: true,
|
||||
hop: {
|
||||
active: false,
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(validateConfig(options)).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
it('should add defaults to missing items', () => {
|
||||
const options = {
|
||||
peerInfo,
|
||||
modules: {
|
||||
transport: [ WS ],
|
||||
peerDiscovery: [ Bootstrap ],
|
||||
dht: DHT
|
||||
},
|
||||
config: {
|
||||
peerDiscovery: {
|
||||
bootstrap: {
|
||||
interval: 1000,
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const expected = {
|
||||
peerInfo,
|
||||
connectionManager: {
|
||||
minPeers: 25
|
||||
},
|
||||
modules: {
|
||||
transport: [ WS ],
|
||||
peerDiscovery: [ Bootstrap ],
|
||||
dht: DHT
|
||||
},
|
||||
config: {
|
||||
peerDiscovery: {
|
||||
autoDial: true,
|
||||
bootstrap: {
|
||||
interval: 1000,
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
EXPERIMENTAL: {
|
||||
pubsub: false
|
||||
},
|
||||
dht: {
|
||||
kBucketSize: 20,
|
||||
enabled: false,
|
||||
randomWalk: {
|
||||
enabled: false,
|
||||
queriesPerPeriod: 1,
|
||||
interval: 300000,
|
||||
timeout: 10000
|
||||
}
|
||||
},
|
||||
relay: {
|
||||
enabled: true,
|
||||
hop: {
|
||||
active: false,
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(validateConfig(options)).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
it('should allow for configuring the switch', () => {
|
||||
const options = {
|
||||
peerInfo,
|
||||
switch: {
|
||||
blacklistTTL: 60e3,
|
||||
blackListAttempts: 5,
|
||||
maxParallelDials: 100,
|
||||
maxColdCalls: 50,
|
||||
dialTimeout: 30e3
|
||||
},
|
||||
modules: {
|
||||
transport: [ WS ],
|
||||
peerDiscovery: [ ]
|
||||
}
|
||||
}
|
||||
|
||||
expect(validateConfig(options)).to.deep.include({
|
||||
switch: {
|
||||
blacklistTTL: 60e3,
|
||||
blackListAttempts: 5,
|
||||
maxParallelDials: 100,
|
||||
maxColdCalls: 50,
|
||||
dialTimeout: 30e3
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('should allow for delegated content and peer routing', () => {
|
||||
const peerRouter = new DelegatedPeerRouter()
|
||||
const contentRouter = new DelegatedContentRouter(peerInfo)
|
||||
|
||||
const options = {
|
||||
peerInfo,
|
||||
modules: {
|
||||
transport: [ WS ],
|
||||
peerDiscovery: [ Bootstrap ],
|
||||
peerRouting: [ peerRouter ],
|
||||
contentRouting: [ contentRouter ],
|
||||
dht: DHT
|
||||
},
|
||||
config: {
|
||||
peerDiscovery: {
|
||||
bootstrap: {
|
||||
interval: 1000,
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(validateConfig(options).modules).to.deep.include({
|
||||
peerRouting: [ peerRouter ],
|
||||
contentRouting: [ contentRouter ]
|
||||
})
|
||||
})
|
||||
|
||||
it('should not allow for dht to be enabled without it being provided', () => {
|
||||
const options = {
|
||||
peerInfo,
|
||||
modules: {
|
||||
transport: [ WS ]
|
||||
},
|
||||
config: {
|
||||
dht: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(() => validateConfig(options)).to.throw()
|
||||
})
|
||||
|
||||
it('should be able to add validators and selectors for dht', () => {
|
||||
const selectors = {}
|
||||
const validators = {}
|
||||
|
||||
const options = {
|
||||
peerInfo,
|
||||
modules: {
|
||||
transport: [WS],
|
||||
dht: DHT
|
||||
},
|
||||
config: {
|
||||
dht: {
|
||||
selectors,
|
||||
validators
|
||||
}
|
||||
}
|
||||
}
|
||||
const expected = {
|
||||
peerInfo,
|
||||
connectionManager: {
|
||||
minPeers: 25
|
||||
},
|
||||
modules: {
|
||||
transport: [WS],
|
||||
dht: DHT
|
||||
},
|
||||
config: {
|
||||
EXPERIMENTAL: {
|
||||
pubsub: false
|
||||
},
|
||||
peerDiscovery: {
|
||||
autoDial: true
|
||||
},
|
||||
relay: {
|
||||
enabled: true,
|
||||
hop: {
|
||||
active: false,
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
dht: {
|
||||
selectors,
|
||||
validators
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(validateConfig(options)).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
it('should support new properties for the dht config', () => {
|
||||
const options = {
|
||||
peerInfo,
|
||||
modules: {
|
||||
transport: [WS],
|
||||
dht: DHT
|
||||
},
|
||||
config: {
|
||||
dht: {
|
||||
kBucketSize: 20,
|
||||
enabled: false,
|
||||
myNewDHTConfigProperty: true,
|
||||
randomWalk: {
|
||||
enabled: false,
|
||||
queriesPerPeriod: 1,
|
||||
interval: 300000,
|
||||
timeout: 10000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const expected = {
|
||||
kBucketSize: 20,
|
||||
enabled: false,
|
||||
myNewDHTConfigProperty: true,
|
||||
randomWalk: {
|
||||
enabled: false,
|
||||
queriesPerPeriod: 1,
|
||||
interval: 300000,
|
||||
timeout: 10000
|
||||
}
|
||||
}
|
||||
|
||||
const actual = validateConfig(options).config.dht
|
||||
|
||||
expect(actual).to.deep.equal(expected)
|
||||
})
|
||||
})
|
133
test/connection-manager/index.spec.js
Normal file
133
test/connection-manager/index.spec.js
Normal file
@ -0,0 +1,133 @@
|
||||
'use strict'
|
||||
/* eslint-env mocha */
|
||||
|
||||
const chai = require('chai')
|
||||
chai.use(require('dirty-chai'))
|
||||
chai.use(require('chai-as-promised'))
|
||||
const { expect } = chai
|
||||
const sinon = require('sinon')
|
||||
|
||||
const { createPeer } = require('../utils/creators/peer')
|
||||
const mockConnection = require('../utils/mockConnection')
|
||||
const baseOptions = require('../utils/base-options.browser')
|
||||
|
||||
describe('Connection Manager', () => {
|
||||
let libp2p
|
||||
|
||||
afterEach(async () => {
|
||||
sinon.restore()
|
||||
libp2p && await libp2p.stop()
|
||||
})
|
||||
|
||||
it('should be able to create without metrics', async () => {
|
||||
[libp2p] = await createPeer({
|
||||
config: {
|
||||
modules: baseOptions.modules
|
||||
},
|
||||
started: false
|
||||
})
|
||||
|
||||
const spy = sinon.spy(libp2p.connectionManager, 'start')
|
||||
|
||||
await libp2p.start()
|
||||
expect(spy).to.have.property('callCount', 1)
|
||||
expect(libp2p.connectionManager._metrics).to.not.exist()
|
||||
})
|
||||
|
||||
it('should be able to create with metrics', async () => {
|
||||
[libp2p] = await createPeer({
|
||||
config: {
|
||||
modules: baseOptions.modules,
|
||||
metrics: {
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
started: false
|
||||
})
|
||||
|
||||
const spy = sinon.spy(libp2p.connectionManager, 'start')
|
||||
|
||||
await libp2p.start()
|
||||
expect(spy).to.have.property('callCount', 1)
|
||||
expect(libp2p.connectionManager._metrics).to.exist()
|
||||
})
|
||||
|
||||
it('should close lowest value peer connection when the maximum has been reached', async () => {
|
||||
const max = 5
|
||||
;[libp2p] = await createPeer({
|
||||
config: {
|
||||
modules: baseOptions.modules,
|
||||
connectionManager: {
|
||||
maxConnections: max
|
||||
}
|
||||
},
|
||||
started: false
|
||||
})
|
||||
|
||||
await libp2p.start()
|
||||
|
||||
sinon.spy(libp2p.connectionManager, '_maybeDisconnectOne')
|
||||
|
||||
// Add 1 too many connections
|
||||
const spies = new Map()
|
||||
await Promise.all([...new Array(max + 1)].map(async (_, index) => {
|
||||
const connection = await mockConnection()
|
||||
const spy = sinon.spy(connection, 'close')
|
||||
// The connections have the same remote id, give them random ones
|
||||
// so that we can verify the correct connection was closed
|
||||
sinon.stub(connection.remotePeer, 'toString').returns(index)
|
||||
const value = Math.random()
|
||||
spies.set(value, spy)
|
||||
libp2p.connectionManager.setPeerValue(connection.remotePeer, value)
|
||||
libp2p.connectionManager.onConnect(connection)
|
||||
}))
|
||||
|
||||
// get the lowest value
|
||||
const lowest = Array.from(spies.keys()).sort()[0]
|
||||
const lowestSpy = spies.get(lowest)
|
||||
|
||||
expect(libp2p.connectionManager._maybeDisconnectOne).to.have.property('callCount', 1)
|
||||
expect(lowestSpy).to.have.property('callCount', 1)
|
||||
})
|
||||
|
||||
it('should close connection when the maximum has been reached even without peer values', async () => {
|
||||
const max = 5
|
||||
;[libp2p] = await createPeer({
|
||||
config: {
|
||||
modules: baseOptions.modules,
|
||||
connectionManager: {
|
||||
maxConnections: max
|
||||
}
|
||||
},
|
||||
started: false
|
||||
})
|
||||
|
||||
await libp2p.start()
|
||||
|
||||
sinon.spy(libp2p.connectionManager, '_maybeDisconnectOne')
|
||||
|
||||
// Add 1 too many connections
|
||||
const spy = sinon.spy()
|
||||
await Promise.all([...new Array(max + 1)].map(async () => {
|
||||
const connection = await mockConnection()
|
||||
sinon.stub(connection, 'close').callsFake(() => spy())
|
||||
libp2p.connectionManager.onConnect(connection)
|
||||
}))
|
||||
|
||||
expect(libp2p.connectionManager._maybeDisconnectOne).to.have.property('callCount', 1)
|
||||
expect(spy).to.have.property('callCount', 1)
|
||||
})
|
||||
|
||||
it('should fail if the connection manager has mismatched connection limit options', async () => {
|
||||
await expect(createPeer({
|
||||
config: {
|
||||
modules: baseOptions.modules,
|
||||
connectionManager: {
|
||||
maxConnections: 5,
|
||||
minConnections: 6
|
||||
}
|
||||
},
|
||||
started: false
|
||||
})).to.eventually.rejected('maxConnections must be greater')
|
||||
})
|
||||
})
|
@ -1,411 +0,0 @@
|
||||
/* eslint-env mocha */
|
||||
/* eslint max-nested-callbacks: ["error", 8] */
|
||||
|
||||
'use strict'
|
||||
|
||||
const chai = require('chai')
|
||||
chai.use(require('dirty-chai'))
|
||||
const expect = chai.expect
|
||||
const parallel = require('async/parallel')
|
||||
const waterfall = require('async/waterfall')
|
||||
const _times = require('lodash.times')
|
||||
const CID = require('cids')
|
||||
const DelegatedContentRouter = require('libp2p-delegated-content-routing')
|
||||
const sinon = require('sinon')
|
||||
const nock = require('nock')
|
||||
const ma = require('multiaddr')
|
||||
const Node = require('./utils/bundle-nodejs')
|
||||
|
||||
const createNode = require('./utils/create-node')
|
||||
const createPeerInfo = createNode.createPeerInfo
|
||||
|
||||
describe('.contentRouting', () => {
|
||||
describe('via the dht', () => {
|
||||
let nodeA
|
||||
let nodeB
|
||||
let nodeC
|
||||
let nodeD
|
||||
let nodeE
|
||||
|
||||
before(function (done) {
|
||||
this.timeout(5 * 1000)
|
||||
const tasks = _times(5, () => (cb) => {
|
||||
createNode('/ip4/0.0.0.0/tcp/0', (err, node) => {
|
||||
expect(err).to.not.exist()
|
||||
node.start((err) => cb(err, node))
|
||||
})
|
||||
})
|
||||
|
||||
parallel(tasks, (err, nodes) => {
|
||||
expect(err).to.not.exist()
|
||||
nodeA = nodes[0]
|
||||
nodeB = nodes[1]
|
||||
nodeC = nodes[2]
|
||||
nodeD = nodes[3]
|
||||
nodeE = nodes[4]
|
||||
|
||||
parallel([
|
||||
(cb) => nodeA.dial(nodeB.peerInfo, cb),
|
||||
(cb) => nodeB.dial(nodeC.peerInfo, cb),
|
||||
(cb) => nodeC.dial(nodeD.peerInfo, cb),
|
||||
(cb) => nodeD.dial(nodeE.peerInfo, cb),
|
||||
(cb) => nodeE.dial(nodeA.peerInfo, cb)
|
||||
], done)
|
||||
})
|
||||
})
|
||||
|
||||
after((done) => {
|
||||
parallel([
|
||||
(cb) => nodeA.stop(cb),
|
||||
(cb) => nodeB.stop(cb),
|
||||
(cb) => nodeC.stop(cb),
|
||||
(cb) => nodeD.stop(cb),
|
||||
(cb) => nodeE.stop(cb)
|
||||
], done)
|
||||
})
|
||||
|
||||
it('should use the nodes dht to provide', (done) => {
|
||||
const stub = sinon.stub(nodeA._dht, 'provide').callsFake(() => {
|
||||
stub.restore()
|
||||
done()
|
||||
})
|
||||
|
||||
nodeA.contentRouting.provide()
|
||||
})
|
||||
|
||||
it('should use the nodes dht to find providers', (done) => {
|
||||
const stub = sinon.stub(nodeA._dht, 'findProviders').callsFake(() => {
|
||||
stub.restore()
|
||||
done()
|
||||
})
|
||||
|
||||
nodeA.contentRouting.findProviders()
|
||||
})
|
||||
|
||||
describe('le ring', () => {
|
||||
const cid = new CID('QmTp9VkYvnHyrqKQuFPiuZkiX9gPcqj6x5LJ1rmWuSySnL')
|
||||
|
||||
it('let kbucket get filled', (done) => {
|
||||
setTimeout(() => done(), 250)
|
||||
})
|
||||
|
||||
it('nodeA.contentRouting.provide', (done) => {
|
||||
nodeA.contentRouting.provide(cid, done)
|
||||
})
|
||||
|
||||
it('nodeE.contentRouting.findProviders for existing record', (done) => {
|
||||
nodeE.contentRouting.findProviders(cid, { maxTimeout: 5000 }, (err, providers) => {
|
||||
expect(err).to.not.exist()
|
||||
expect(providers).to.have.length.above(0)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('nodeE.contentRouting.findProviders with limited number of providers', (done) => {
|
||||
parallel([
|
||||
(cb) => nodeA.contentRouting.provide(cid, cb),
|
||||
(cb) => nodeB.contentRouting.provide(cid, cb),
|
||||
(cb) => nodeC.contentRouting.provide(cid, cb)
|
||||
], (err) => {
|
||||
expect(err).to.not.exist()
|
||||
|
||||
nodeE.contentRouting.findProviders(cid, { maxNumProviders: 2 }, (err, providers) => {
|
||||
expect(err).to.not.exist()
|
||||
expect(providers).to.have.length(2)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('nodeC.contentRouting.findProviders for non existing record (timeout)', (done) => {
|
||||
const cid = new CID('QmTp9VkYvnHyrqKQuFPiuZkiX9gPcqj6x5LJ1rmWuSnnnn')
|
||||
|
||||
nodeE.contentRouting.findProviders(cid, { maxTimeout: 5000 }, (err, providers) => {
|
||||
expect(err).to.not.exist()
|
||||
expect(providers).to.have.length(0)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('via a delegate', () => {
|
||||
let nodeA
|
||||
let delegate
|
||||
|
||||
before((done) => {
|
||||
waterfall([
|
||||
(cb) => {
|
||||
createPeerInfo(cb)
|
||||
},
|
||||
// Create the node using the delegate
|
||||
(peerInfo, cb) => {
|
||||
delegate = new DelegatedContentRouter(peerInfo.id, {
|
||||
host: '0.0.0.0',
|
||||
protocol: 'http',
|
||||
port: 60197
|
||||
}, [
|
||||
ma('/ip4/0.0.0.0/tcp/60194')
|
||||
])
|
||||
nodeA = new Node({
|
||||
peerInfo,
|
||||
modules: {
|
||||
contentRouting: [ delegate ]
|
||||
},
|
||||
config: {
|
||||
dht: {
|
||||
enabled: false
|
||||
},
|
||||
relay: {
|
||||
enabled: true,
|
||||
hop: {
|
||||
enabled: true,
|
||||
active: false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
nodeA.start(cb)
|
||||
}
|
||||
], done)
|
||||
})
|
||||
|
||||
after((done) => nodeA.stop(done))
|
||||
afterEach(() => nock.cleanAll())
|
||||
|
||||
describe('provide', () => {
|
||||
it('should use the delegate router to provide', (done) => {
|
||||
const stub = sinon.stub(delegate, 'provide').callsFake(() => {
|
||||
stub.restore()
|
||||
done()
|
||||
})
|
||||
nodeA.contentRouting.provide()
|
||||
})
|
||||
|
||||
it('should be able to register as a provider', (done) => {
|
||||
const cid = new CID('QmU621oD8AhHw6t25vVyfYKmL9VV3PTgc52FngEhTGACFB')
|
||||
const mockApi = nock('http://0.0.0.0:60197')
|
||||
// mock the swarm connect
|
||||
.post('/api/v0/swarm/connect')
|
||||
.query({
|
||||
arg: `/ip4/0.0.0.0/tcp/60194/p2p-circuit/ipfs/${nodeA.peerInfo.id.toB58String()}`,
|
||||
'stream-channels': true
|
||||
})
|
||||
.reply(200, {
|
||||
Strings: [`connect ${nodeA.peerInfo.id.toB58String()} success`]
|
||||
}, ['Content-Type', 'application/json'])
|
||||
// mock the refs call
|
||||
.post('/api/v0/refs')
|
||||
.query({
|
||||
recursive: true,
|
||||
arg: cid.toBaseEncodedString(),
|
||||
'stream-channels': true
|
||||
})
|
||||
.reply(200, null, [
|
||||
'Content-Type', 'application/json',
|
||||
'X-Chunked-Output', '1'
|
||||
])
|
||||
|
||||
nodeA.contentRouting.provide(cid, (err) => {
|
||||
expect(err).to.not.exist()
|
||||
expect(mockApi.isDone()).to.equal(true)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle errors when registering as a provider', (done) => {
|
||||
const cid = new CID('QmU621oD8AhHw6t25vVyfYKmL9VV3PTgc52FngEhTGACFB')
|
||||
const mockApi = nock('http://0.0.0.0:60197')
|
||||
// mock the swarm connect
|
||||
.post('/api/v0/swarm/connect')
|
||||
.query({
|
||||
arg: `/ip4/0.0.0.0/tcp/60194/p2p-circuit/ipfs/${nodeA.peerInfo.id.toB58String()}`,
|
||||
'stream-channels': true
|
||||
})
|
||||
.reply(502, 'Bad Gateway', ['Content-Type', 'application/json'])
|
||||
|
||||
nodeA.contentRouting.provide(cid, (err) => {
|
||||
expect(err).to.exist()
|
||||
expect(mockApi.isDone()).to.equal(true)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('find providers', () => {
|
||||
it('should use the delegate router to find providers', (done) => {
|
||||
const stub = sinon.stub(delegate, 'findProviders').callsFake(() => {
|
||||
stub.restore()
|
||||
done()
|
||||
})
|
||||
nodeA.contentRouting.findProviders()
|
||||
})
|
||||
|
||||
it('should be able to find providers', (done) => {
|
||||
const cid = new CID('QmU621oD8AhHw6t25vVyfYKmL9VV3PTgc52FngEhTGACFB')
|
||||
const provider = 'QmZNgCqZCvTsi3B4Vt7gsSqpkqDpE7M2Y9TDmEhbDb4ceF'
|
||||
const mockApi = nock('http://0.0.0.0:60197')
|
||||
.post('/api/v0/dht/findprovs')
|
||||
.query({
|
||||
arg: cid.toBaseEncodedString(),
|
||||
timeout: '1000ms',
|
||||
'stream-channels': true
|
||||
})
|
||||
.reply(200, `{"Extra":"","ID":"QmWKqWXCtRXEeCQTo3FoZ7g4AfnGiauYYiczvNxFCHicbB","Responses":[{"Addrs":["/ip4/0.0.0.0/tcp/0"],"ID":"${provider}"}],"Type":1}\n`, [
|
||||
'Content-Type', 'application/json',
|
||||
'X-Chunked-Output', '1'
|
||||
])
|
||||
|
||||
nodeA.contentRouting.findProviders(cid, 1000, (err, response) => {
|
||||
expect(err).to.not.exist()
|
||||
expect(response).to.have.length(1)
|
||||
expect(response[0].id.toB58String()).to.equal(provider)
|
||||
expect(mockApi.isDone()).to.equal(true)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle errors when finding providers', (done) => {
|
||||
const cid = new CID('QmU621oD8AhHw6t25vVyfYKmL9VV3PTgc52FngEhTGACFB')
|
||||
const mockApi = nock('http://0.0.0.0:60197')
|
||||
.post('/api/v0/dht/findprovs')
|
||||
.query({
|
||||
arg: cid.toBaseEncodedString(),
|
||||
timeout: '30000ms',
|
||||
'stream-channels': true
|
||||
})
|
||||
.reply(502, 'Bad Gateway', [
|
||||
'X-Chunked-Output', '1'
|
||||
])
|
||||
|
||||
nodeA.contentRouting.findProviders(cid, (err) => {
|
||||
expect(err).to.exist()
|
||||
expect(mockApi.isDone()).to.equal(true)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('via the dht and a delegate', () => {
|
||||
let nodeA
|
||||
let delegate
|
||||
|
||||
before((done) => {
|
||||
waterfall([
|
||||
(cb) => {
|
||||
createPeerInfo(cb)
|
||||
},
|
||||
// Create the node using the delegate
|
||||
(peerInfo, cb) => {
|
||||
delegate = new DelegatedContentRouter(peerInfo.id, {
|
||||
host: '0.0.0.0',
|
||||
protocol: 'http',
|
||||
port: 60197
|
||||
}, [
|
||||
ma('/ip4/0.0.0.0/tcp/60194')
|
||||
])
|
||||
nodeA = new Node({
|
||||
peerInfo,
|
||||
modules: {
|
||||
contentRouting: [ delegate ]
|
||||
},
|
||||
config: {
|
||||
relay: {
|
||||
enabled: true,
|
||||
hop: {
|
||||
enabled: true,
|
||||
active: false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
nodeA.start(cb)
|
||||
}
|
||||
], done)
|
||||
})
|
||||
|
||||
after((done) => nodeA.stop(done))
|
||||
|
||||
describe('provide', () => {
|
||||
it('should use both the dht and delegate router to provide', (done) => {
|
||||
const dhtStub = sinon.stub(nodeA._dht, 'provide').callsFake(() => {})
|
||||
const delegateStub = sinon.stub(delegate, 'provide').callsFake(() => {
|
||||
expect(dhtStub.calledOnce).to.equal(true)
|
||||
expect(delegateStub.calledOnce).to.equal(true)
|
||||
delegateStub.restore()
|
||||
dhtStub.restore()
|
||||
done()
|
||||
})
|
||||
nodeA.contentRouting.provide()
|
||||
})
|
||||
})
|
||||
|
||||
describe('findProviders', () => {
|
||||
it('should only use the dht if it finds providers', (done) => {
|
||||
const results = [true]
|
||||
const dhtStub = sinon.stub(nodeA._dht, 'findProviders').callsArgWith(2, null, results)
|
||||
const delegateStub = sinon.stub(delegate, 'findProviders').throws(() => {
|
||||
return new Error('the delegate should not have been called')
|
||||
})
|
||||
|
||||
nodeA.contentRouting.findProviders('a cid', { maxTimeout: 5000 }, (err, results) => {
|
||||
expect(err).to.not.exist()
|
||||
expect(results).to.equal(results)
|
||||
expect(dhtStub.calledOnce).to.equal(true)
|
||||
expect(delegateStub.notCalled).to.equal(true)
|
||||
delegateStub.restore()
|
||||
dhtStub.restore()
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should use the delegate if the dht fails to find providers', (done) => {
|
||||
const results = [true]
|
||||
const dhtStub = sinon.stub(nodeA._dht, 'findProviders').callsArgWith(2, null, [])
|
||||
const delegateStub = sinon.stub(delegate, 'findProviders').callsArgWith(2, null, results)
|
||||
|
||||
nodeA.contentRouting.findProviders('a cid', { maxTimeout: 5000 }, (err, results) => {
|
||||
expect(err).to.not.exist()
|
||||
expect(results).to.deep.equal(results)
|
||||
expect(dhtStub.calledOnce).to.equal(true)
|
||||
expect(delegateStub.calledOnce).to.equal(true)
|
||||
delegateStub.restore()
|
||||
dhtStub.restore()
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('no routers', () => {
|
||||
let nodeA
|
||||
before((done) => {
|
||||
createNode('/ip4/0.0.0.0/tcp/0', {
|
||||
config: {
|
||||
dht: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
}, (err, node) => {
|
||||
expect(err).to.not.exist()
|
||||
nodeA = node
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('.findProviders should return an error with no options', (done) => {
|
||||
nodeA.contentRouting.findProviders('a cid', (err) => {
|
||||
expect(err).to.exist()
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('.findProviders should return an error with options', (done) => {
|
||||
nodeA.contentRouting.findProviders('a cid', { maxTimeout: 5000 }, (err) => {
|
||||
expect(err).to.exist()
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
323
test/content-routing/content-routing.node.js
Normal file
323
test/content-routing/content-routing.node.js
Normal file
@ -0,0 +1,323 @@
|
||||
'use strict'
|
||||
/* eslint-env mocha */
|
||||
|
||||
const chai = require('chai')
|
||||
chai.use(require('dirty-chai'))
|
||||
const { expect } = chai
|
||||
const nock = require('nock')
|
||||
const sinon = require('sinon')
|
||||
|
||||
const pDefer = require('p-defer')
|
||||
const mergeOptions = require('merge-options')
|
||||
|
||||
const CID = require('cids')
|
||||
const DelegatedContentRouter = require('libp2p-delegated-content-routing')
|
||||
const multiaddr = require('multiaddr')
|
||||
|
||||
const peerUtils = require('../utils/creators/peer')
|
||||
const { baseOptions, routingOptions } = require('./utils')
|
||||
|
||||
describe('content-routing', () => {
|
||||
describe('no routers', () => {
|
||||
let node
|
||||
|
||||
before(async () => {
|
||||
[node] = await peerUtils.createPeer({
|
||||
config: baseOptions
|
||||
})
|
||||
})
|
||||
|
||||
it('.findProviders should return an error', async () => {
|
||||
try {
|
||||
for await (const _ of node.contentRouting.findProviders('a cid')) {} // eslint-disable-line
|
||||
throw new Error('.findProviders should return an error')
|
||||
} catch (err) {
|
||||
expect(err).to.exist()
|
||||
expect(err.code).to.equal('NO_ROUTERS_AVAILABLE')
|
||||
}
|
||||
})
|
||||
|
||||
it('.provide should return an error', async () => {
|
||||
await expect(node.contentRouting.provide('a cid'))
|
||||
.to.eventually.be.rejected()
|
||||
.and.to.have.property('code', 'NO_ROUTERS_AVAILABLE')
|
||||
})
|
||||
})
|
||||
|
||||
describe('via dht router', () => {
|
||||
const number = 5
|
||||
let nodes
|
||||
|
||||
before(async () => {
|
||||
nodes = await peerUtils.createPeer({
|
||||
number,
|
||||
config: routingOptions
|
||||
})
|
||||
|
||||
// Ring dial
|
||||
await Promise.all(
|
||||
nodes.map((peer, i) => peer.dial(nodes[(i + 1) % number].peerInfo))
|
||||
)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
after(() => Promise.all(nodes.map((n) => n.stop())))
|
||||
|
||||
it('should use the nodes dht to provide', () => {
|
||||
const deferred = pDefer()
|
||||
|
||||
sinon.stub(nodes[0]._dht, 'provide').callsFake(() => {
|
||||
deferred.resolve()
|
||||
})
|
||||
|
||||
nodes[0].contentRouting.provide()
|
||||
return deferred.promise
|
||||
})
|
||||
|
||||
it('should use the nodes dht to find providers', async () => {
|
||||
const deferred = pDefer()
|
||||
|
||||
sinon.stub(nodes[0]._dht, 'findProviders').callsFake(function * () {
|
||||
deferred.resolve()
|
||||
yield
|
||||
})
|
||||
|
||||
await nodes[0].contentRouting.findProviders().next()
|
||||
|
||||
return deferred.promise
|
||||
})
|
||||
})
|
||||
|
||||
describe('via delegate router', () => {
|
||||
let node
|
||||
let delegate
|
||||
|
||||
beforeEach(async () => {
|
||||
const [peerInfo] = await peerUtils.createPeerInfo({ fixture: false })
|
||||
|
||||
delegate = new DelegatedContentRouter(peerInfo.id, {
|
||||
host: '0.0.0.0',
|
||||
protocol: 'http',
|
||||
port: 60197
|
||||
}, [
|
||||
multiaddr('/ip4/0.0.0.0/tcp/60197')
|
||||
])
|
||||
|
||||
;[node] = await peerUtils.createPeer({
|
||||
config: mergeOptions(baseOptions, {
|
||||
modules: {
|
||||
contentRouting: [delegate]
|
||||
},
|
||||
config: {
|
||||
dht: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
afterEach(() => node.stop())
|
||||
|
||||
it('should use the delegate router to provide', () => {
|
||||
const deferred = pDefer()
|
||||
|
||||
sinon.stub(delegate, 'provide').callsFake(() => {
|
||||
deferred.resolve()
|
||||
})
|
||||
|
||||
node.contentRouting.provide()
|
||||
return deferred.promise
|
||||
})
|
||||
|
||||
it('should use the delegate router to find providers', async () => {
|
||||
const deferred = pDefer()
|
||||
|
||||
sinon.stub(delegate, 'findProviders').callsFake(function * () {
|
||||
deferred.resolve()
|
||||
yield
|
||||
})
|
||||
|
||||
await node.contentRouting.findProviders().next()
|
||||
|
||||
return deferred.promise
|
||||
})
|
||||
|
||||
it('should be able to register as a provider', async () => {
|
||||
const cid = new CID('QmU621oD8AhHw6t25vVyfYKmL9VV3PTgc52FngEhTGACFB')
|
||||
const mockApi = nock('http://0.0.0.0:60197')
|
||||
// mock the refs call
|
||||
.post('/api/v0/refs')
|
||||
.query({
|
||||
recursive: false,
|
||||
arg: cid.toBaseEncodedString()
|
||||
})
|
||||
.reply(200, null, [
|
||||
'Content-Type', 'application/json',
|
||||
'X-Chunked-Output', '1'
|
||||
])
|
||||
|
||||
await node.contentRouting.provide(cid)
|
||||
|
||||
expect(mockApi.isDone()).to.equal(true)
|
||||
})
|
||||
|
||||
it('should handle errors when registering as a provider', async () => {
|
||||
const cid = new CID('QmU621oD8AhHw6t25vVyfYKmL9VV3PTgc52FngEhTGACFB')
|
||||
const mockApi = nock('http://0.0.0.0:60197')
|
||||
// mock the refs call
|
||||
.post('/api/v0/refs')
|
||||
.query({
|
||||
recursive: false,
|
||||
arg: cid.toBaseEncodedString()
|
||||
})
|
||||
.reply(502, 'Bad Gateway', ['Content-Type', 'application/json'])
|
||||
|
||||
await expect(node.contentRouting.provide(cid))
|
||||
.to.eventually.be.rejected()
|
||||
|
||||
expect(mockApi.isDone()).to.equal(true)
|
||||
})
|
||||
|
||||
it('should be able to find providers', async () => {
|
||||
const cid = new CID('QmU621oD8AhHw6t25vVyfYKmL9VV3PTgc52FngEhTGACFB')
|
||||
const provider = 'QmZNgCqZCvTsi3B4Vt7gsSqpkqDpE7M2Y9TDmEhbDb4ceF'
|
||||
|
||||
const mockApi = nock('http://0.0.0.0:60197')
|
||||
.post('/api/v0/dht/findprovs')
|
||||
.query({
|
||||
arg: cid.toBaseEncodedString()
|
||||
})
|
||||
.reply(200, `{"Extra":"","ID":"QmWKqWXCtRXEeCQTo3FoZ7g4AfnGiauYYiczvNxFCHicbB","Responses":[{"Addrs":["/ip4/0.0.0.0/tcp/0"],"ID":"${provider}"}],"Type":4}\n`, [
|
||||
'Content-Type', 'application/json',
|
||||
'X-Chunked-Output', '1'
|
||||
])
|
||||
|
||||
const providers = []
|
||||
for await (const provider of node.contentRouting.findProviders(cid, { timeout: 1000 })) {
|
||||
providers.push(provider)
|
||||
}
|
||||
|
||||
expect(providers).to.have.length(1)
|
||||
expect(providers[0].id.toB58String()).to.equal(provider)
|
||||
expect(mockApi.isDone()).to.equal(true)
|
||||
})
|
||||
|
||||
it('should handle errors when finding providers', async () => {
|
||||
const cid = new CID('QmU621oD8AhHw6t25vVyfYKmL9VV3PTgc52FngEhTGACFB')
|
||||
const mockApi = nock('http://0.0.0.0:60197')
|
||||
.post('/api/v0/dht/findprovs')
|
||||
.query({
|
||||
arg: cid.toBaseEncodedString()
|
||||
})
|
||||
.reply(502, 'Bad Gateway', [
|
||||
'X-Chunked-Output', '1'
|
||||
])
|
||||
|
||||
try {
|
||||
for await (const _ of node.contentRouting.findProviders(cid)) { } // eslint-disable-line
|
||||
throw new Error('should handle errors when finding providers')
|
||||
} catch (err) {
|
||||
expect(err).to.exist()
|
||||
}
|
||||
|
||||
expect(mockApi.isDone()).to.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('via dht and delegate routers', () => {
|
||||
let node
|
||||
let delegate
|
||||
|
||||
beforeEach(async () => {
|
||||
const [peerInfo] = await peerUtils.createPeerInfo({ fixture: false })
|
||||
|
||||
delegate = new DelegatedContentRouter(peerInfo.id, {
|
||||
host: '0.0.0.0',
|
||||
protocol: 'http',
|
||||
port: 60197
|
||||
}, [
|
||||
multiaddr('/ip4/0.0.0.0/tcp/60197')
|
||||
])
|
||||
|
||||
;[node] = await peerUtils.createPeer({
|
||||
config: mergeOptions(routingOptions, {
|
||||
modules: {
|
||||
contentRouting: [delegate]
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
afterEach(() => node.stop())
|
||||
|
||||
it('should use both the dht and delegate router to provide', async () => {
|
||||
const dhtDeferred = pDefer()
|
||||
const delegatedDeferred = pDefer()
|
||||
|
||||
sinon.stub(node._dht, 'provide').callsFake(() => {
|
||||
dhtDeferred.resolve()
|
||||
})
|
||||
|
||||
sinon.stub(delegate, 'provide').callsFake(() => {
|
||||
delegatedDeferred.resolve()
|
||||
})
|
||||
|
||||
await node.contentRouting.provide()
|
||||
|
||||
await Promise.all([
|
||||
dhtDeferred.promise,
|
||||
delegatedDeferred.promise
|
||||
])
|
||||
})
|
||||
|
||||
it('should only use the dht if it finds providers', async () => {
|
||||
const results = [true]
|
||||
|
||||
sinon.stub(node._dht, 'findProviders').callsFake(function * () {
|
||||
yield results[0]
|
||||
})
|
||||
|
||||
sinon.stub(delegate, 'findProviders').callsFake(function * () { // eslint-disable-line require-yield
|
||||
throw new Error('the delegate should not have been called')
|
||||
})
|
||||
|
||||
const providers = []
|
||||
for await (const prov of node.contentRouting.findProviders('a cid')) {
|
||||
providers.push(prov)
|
||||
}
|
||||
|
||||
expect(providers).to.have.length.above(0)
|
||||
expect(providers).to.eql(results)
|
||||
})
|
||||
|
||||
it('should use the delegate if the dht fails to find providers', async () => {
|
||||
const results = [true]
|
||||
|
||||
sinon.stub(node._dht, 'findProviders').callsFake(function * () {})
|
||||
|
||||
sinon.stub(delegate, 'findProviders').callsFake(function * () {
|
||||
yield results[0]
|
||||
})
|
||||
|
||||
const providers = []
|
||||
for await (const prov of node.contentRouting.findProviders('a cid')) {
|
||||
providers.push(prov)
|
||||
}
|
||||
|
||||
expect(providers).to.have.length.above(0)
|
||||
expect(providers).to.eql(results)
|
||||
})
|
||||
})
|
||||
})
|
92
test/content-routing/dht/configuration.node.js
Normal file
92
test/content-routing/dht/configuration.node.js
Normal file
@ -0,0 +1,92 @@
|
||||
'use strict'
|
||||
/* eslint-env mocha */
|
||||
|
||||
const chai = require('chai')
|
||||
chai.use(require('dirty-chai'))
|
||||
const { expect } = chai
|
||||
|
||||
const mergeOptions = require('merge-options')
|
||||
const multiaddr = require('multiaddr')
|
||||
|
||||
const { create } = require('../../../src')
|
||||
const { baseOptions, subsystemOptions } = require('./utils')
|
||||
const peerUtils = require('../../utils/creators/peer')
|
||||
|
||||
const listenAddr = multiaddr('/ip4/127.0.0.1/tcp/0')
|
||||
|
||||
describe('DHT subsystem is configurable', () => {
|
||||
let libp2p
|
||||
|
||||
afterEach(async () => {
|
||||
libp2p && await libp2p.stop()
|
||||
})
|
||||
|
||||
it('should not exist if no module is provided', async () => {
|
||||
libp2p = await create(baseOptions)
|
||||
expect(libp2p._dht).to.not.exist()
|
||||
})
|
||||
|
||||
it('should exist if the module is provided', async () => {
|
||||
libp2p = await create(subsystemOptions)
|
||||
expect(libp2p._dht).to.exist()
|
||||
})
|
||||
|
||||
it('should start and stop by default once libp2p starts', async () => {
|
||||
const [peerInfo] = await peerUtils.createPeerInfo(1)
|
||||
peerInfo.multiaddrs.add(listenAddr)
|
||||
|
||||
const customOptions = mergeOptions(subsystemOptions, {
|
||||
peerInfo
|
||||
})
|
||||
|
||||
libp2p = await create(customOptions)
|
||||
expect(libp2p._dht.isStarted).to.equal(false)
|
||||
|
||||
await libp2p.start()
|
||||
expect(libp2p._dht.isStarted).to.equal(true)
|
||||
|
||||
await libp2p.stop()
|
||||
expect(libp2p._dht.isStarted).to.equal(false)
|
||||
})
|
||||
|
||||
it('should not start if disabled once libp2p starts', async () => {
|
||||
const [peerInfo] = await peerUtils.createPeerInfo(1)
|
||||
peerInfo.multiaddrs.add(listenAddr)
|
||||
|
||||
const customOptions = mergeOptions(subsystemOptions, {
|
||||
peerInfo,
|
||||
config: {
|
||||
dht: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
libp2p = await create(customOptions)
|
||||
expect(libp2p._dht.isStarted).to.equal(false)
|
||||
|
||||
await libp2p.start()
|
||||
expect(libp2p._dht.isStarted).to.equal(false)
|
||||
})
|
||||
|
||||
it('should allow a manual start', async () => {
|
||||
const [peerInfo] = await peerUtils.createPeerInfo(1)
|
||||
peerInfo.multiaddrs.add(listenAddr)
|
||||
|
||||
const customOptions = mergeOptions(subsystemOptions, {
|
||||
peerInfo,
|
||||
config: {
|
||||
dht: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
libp2p = await create(customOptions)
|
||||
await libp2p.start()
|
||||
expect(libp2p._dht.isStarted).to.equal(false)
|
||||
|
||||
await libp2p._dht.start()
|
||||
expect(libp2p._dht.isStarted).to.equal(true)
|
||||
})
|
||||
})
|
135
test/content-routing/dht/operation.node.js
Normal file
135
test/content-routing/dht/operation.node.js
Normal file
@ -0,0 +1,135 @@
|
||||
'use strict'
|
||||
/* eslint-env mocha */
|
||||
|
||||
const chai = require('chai')
|
||||
chai.use(require('dirty-chai'))
|
||||
const { expect } = chai
|
||||
|
||||
const pWaitFor = require('p-wait-for')
|
||||
const mergeOptions = require('merge-options')
|
||||
const multiaddr = require('multiaddr')
|
||||
|
||||
const { create } = require('../../../src')
|
||||
const { subsystemOptions, subsystemMulticodecs } = require('./utils')
|
||||
const peerUtils = require('../../utils/creators/peer')
|
||||
|
||||
const listenAddr = multiaddr('/ip4/127.0.0.1/tcp/8000')
|
||||
const remoteListenAddr = multiaddr('/ip4/127.0.0.1/tcp/8001')
|
||||
|
||||
describe('DHT subsystem operates correctly', () => {
|
||||
let peerInfo, remotePeerInfo
|
||||
let libp2p, remoteLibp2p
|
||||
let remAddr
|
||||
|
||||
beforeEach(async () => {
|
||||
[peerInfo, remotePeerInfo] = await peerUtils.createPeerInfo({ number: 2 })
|
||||
|
||||
peerInfo.multiaddrs.add(listenAddr)
|
||||
remotePeerInfo.multiaddrs.add(remoteListenAddr)
|
||||
})
|
||||
|
||||
describe('dht started before connect', () => {
|
||||
beforeEach(async () => {
|
||||
libp2p = await create(mergeOptions(subsystemOptions, {
|
||||
peerInfo
|
||||
}))
|
||||
|
||||
remoteLibp2p = await create(mergeOptions(subsystemOptions, {
|
||||
peerInfo: remotePeerInfo
|
||||
}))
|
||||
|
||||
await Promise.all([
|
||||
libp2p.start(),
|
||||
remoteLibp2p.start()
|
||||
])
|
||||
|
||||
remAddr = remoteLibp2p.transportManager.getAddrs()[0]
|
||||
})
|
||||
|
||||
afterEach(() => Promise.all([
|
||||
libp2p && libp2p.stop(),
|
||||
remoteLibp2p && remoteLibp2p.stop()
|
||||
]))
|
||||
|
||||
it('should get notified of connected peers on dial', async () => {
|
||||
const connection = await libp2p.dialProtocol(remAddr, subsystemMulticodecs)
|
||||
|
||||
expect(connection).to.exist()
|
||||
|
||||
return Promise.all([
|
||||
pWaitFor(() => libp2p._dht.routingTable.size === 1),
|
||||
pWaitFor(() => remoteLibp2p._dht.routingTable.size === 1)
|
||||
])
|
||||
})
|
||||
|
||||
it('should put on a peer and get from the other', async () => {
|
||||
const key = Buffer.from('hello')
|
||||
const value = Buffer.from('world')
|
||||
|
||||
await libp2p.dialProtocol(remAddr, subsystemMulticodecs)
|
||||
|
||||
await Promise.all([
|
||||
pWaitFor(() => libp2p._dht.routingTable.size === 1),
|
||||
pWaitFor(() => remoteLibp2p._dht.routingTable.size === 1)
|
||||
])
|
||||
|
||||
await libp2p.contentRouting.put(key, value)
|
||||
|
||||
const fetchedValue = await remoteLibp2p.contentRouting.get(key)
|
||||
expect(fetchedValue).to.eql(value)
|
||||
})
|
||||
})
|
||||
|
||||
describe('dht started after connect', () => {
|
||||
beforeEach(async () => {
|
||||
libp2p = await create(mergeOptions(subsystemOptions, {
|
||||
peerInfo
|
||||
}))
|
||||
|
||||
remoteLibp2p = await create(mergeOptions(subsystemOptions, {
|
||||
peerInfo: remotePeerInfo,
|
||||
config: {
|
||||
dht: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
await libp2p.start()
|
||||
await remoteLibp2p.start()
|
||||
|
||||
remAddr = remoteLibp2p.transportManager.getAddrs()[0]
|
||||
})
|
||||
|
||||
afterEach(() => Promise.all([
|
||||
libp2p && libp2p.stop(),
|
||||
remoteLibp2p && remoteLibp2p.stop()
|
||||
]))
|
||||
|
||||
it('should get notified of connected peers after starting', async () => {
|
||||
const connection = await libp2p.dial(remAddr)
|
||||
|
||||
expect(connection).to.exist()
|
||||
expect(libp2p._dht.routingTable.size).to.be.eql(0)
|
||||
expect(remoteLibp2p._dht.routingTable.size).to.be.eql(0)
|
||||
|
||||
await remoteLibp2p._dht.start()
|
||||
return pWaitFor(() => libp2p._dht.routingTable.size === 1)
|
||||
})
|
||||
|
||||
it('should put on a peer and get from the other', async () => {
|
||||
await libp2p.dial(remAddr)
|
||||
|
||||
const key = Buffer.from('hello')
|
||||
const value = Buffer.from('world')
|
||||
|
||||
await remoteLibp2p._dht.start()
|
||||
await pWaitFor(() => libp2p._dht.routingTable.size === 1)
|
||||
|
||||
await libp2p.contentRouting.put(key, value)
|
||||
|
||||
const fetchedValue = await remoteLibp2p.contentRouting.get(key)
|
||||
expect(fetchedValue).to.eql(value)
|
||||
})
|
||||
})
|
||||
})
|
37
test/content-routing/dht/utils.js
Normal file
37
test/content-routing/dht/utils.js
Normal file
@ -0,0 +1,37 @@
|
||||
'use strict'
|
||||
|
||||
const KadDht = require('libp2p-kad-dht')
|
||||
const { multicodec } = require('libp2p-kad-dht')
|
||||
const Crypto = require('../../../src/insecure/plaintext')
|
||||
const Muxer = require('libp2p-mplex')
|
||||
const Transport = require('libp2p-tcp')
|
||||
|
||||
const mergeOptions = require('merge-options')
|
||||
|
||||
const baseOptions = {
|
||||
modules: {
|
||||
transport: [Transport],
|
||||
streamMuxer: [Muxer],
|
||||
connEncryption: [Crypto]
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.baseOptions = baseOptions
|
||||
|
||||
const subsystemOptions = mergeOptions(baseOptions, {
|
||||
modules: {
|
||||
dht: KadDht
|
||||
},
|
||||
config: {
|
||||
dht: {
|
||||
kBucketSize: 20,
|
||||
randomWalk: {
|
||||
enabled: true
|
||||
},
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
module.exports.subsystemOptions = subsystemOptions
|
||||
module.exports.subsystemMulticodecs = [multicodec]
|
24
test/content-routing/utils.js
Normal file
24
test/content-routing/utils.js
Normal file
@ -0,0 +1,24 @@
|
||||
'use strict'
|
||||
|
||||
const KadDht = require('libp2p-kad-dht')
|
||||
const mergeOptions = require('merge-options')
|
||||
const baseOptions = require('../utils/base-options')
|
||||
|
||||
module.exports.baseOptions = baseOptions
|
||||
|
||||
const routingOptions = mergeOptions(baseOptions, {
|
||||
modules: {
|
||||
dht: KadDht
|
||||
},
|
||||
config: {
|
||||
dht: {
|
||||
kBucketSize: 20,
|
||||
randomWalk: {
|
||||
enabled: true
|
||||
},
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
module.exports.routingOptions = routingOptions
|
56
test/core/listening.node.js
Normal file
56
test/core/listening.node.js
Normal file
@ -0,0 +1,56 @@
|
||||
'use strict'
|
||||
/* eslint-env mocha */
|
||||
|
||||
const chai = require('chai')
|
||||
chai.use(require('dirty-chai'))
|
||||
const { expect } = chai
|
||||
|
||||
const multiaddr = require('multiaddr')
|
||||
const Transport = require('libp2p-tcp')
|
||||
|
||||
const { create } = require('../../src')
|
||||
const peerUtils = require('../utils/creators/peer')
|
||||
|
||||
const listenAddr = multiaddr('/ip4/0.0.0.0/tcp/0')
|
||||
|
||||
describe('Listening', () => {
|
||||
let peerInfo
|
||||
let libp2p
|
||||
|
||||
before(async () => {
|
||||
[peerInfo] = await peerUtils.createPeerInfo()
|
||||
peerInfo.multiaddrs.add(listenAddr)
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await libp2p.stop()
|
||||
})
|
||||
|
||||
it('should replace wildcard host and port with actual host and port on startup', async () => {
|
||||
libp2p = await create({
|
||||
peerInfo,
|
||||
modules: {
|
||||
transport: [Transport]
|
||||
}
|
||||
})
|
||||
|
||||
await libp2p.start()
|
||||
|
||||
const addrs = libp2p.peerInfo.multiaddrs.toArray()
|
||||
|
||||
// Should get something like:
|
||||
// /ip4/127.0.0.1/tcp/50866
|
||||
// /ip4/192.168.1.2/tcp/50866
|
||||
expect(addrs.length).to.equal(2)
|
||||
|
||||
const opts = [addrs[0].toOptions(), addrs[1].toOptions()]
|
||||
expect(opts[0].family).to.equal('ipv4')
|
||||
expect(opts[1].family).to.equal('ipv4')
|
||||
expect(opts[0].transport).to.equal('tcp')
|
||||
expect(opts[1].transport).to.equal('tcp')
|
||||
expect(opts[0].host).to.match(/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/)
|
||||
expect(opts[1].host).to.match(/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/)
|
||||
expect(opts[0].port).to.be.gt(0)
|
||||
expect(opts[1].port).to.be.gt(0)
|
||||
})
|
||||
})
|
35
test/core/ping.node.js
Normal file
35
test/core/ping.node.js
Normal file
@ -0,0 +1,35 @@
|
||||
'use strict'
|
||||
/* eslint-env mocha */
|
||||
|
||||
const chai = require('chai')
|
||||
chai.use(require('dirty-chai'))
|
||||
const { expect } = chai
|
||||
|
||||
const pTimes = require('p-times')
|
||||
|
||||
const peerUtils = require('../utils/creators/peer')
|
||||
const baseOptions = require('../utils/base-options')
|
||||
|
||||
describe('ping', () => {
|
||||
let nodes
|
||||
|
||||
beforeEach(async () => {
|
||||
nodes = await peerUtils.createPeer({
|
||||
number: 2,
|
||||
config: baseOptions
|
||||
})
|
||||
})
|
||||
|
||||
it('ping once from peer0 to peer1', async () => {
|
||||
const latency = await nodes[0].ping(nodes[1].peerInfo)
|
||||
|
||||
expect(latency).to.be.a('Number')
|
||||
})
|
||||
|
||||
it('ping several times for getting an average', async () => {
|
||||
const latencies = await pTimes(5, () => nodes[1].ping(nodes[0].peerInfo))
|
||||
|
||||
const averageLatency = latencies.reduce((p, c) => p + c, 0) / latencies.length
|
||||
expect(averageLatency).to.be.a('Number')
|
||||
})
|
||||
})
|
@ -1,143 +0,0 @@
|
||||
/* eslint-env mocha */
|
||||
'use strict'
|
||||
|
||||
const chai = require('chai')
|
||||
chai.use(require('dirty-chai'))
|
||||
const expect = chai.expect
|
||||
const series = require('async/series')
|
||||
const createNode = require('./utils/create-node')
|
||||
const sinon = require('sinon')
|
||||
const { createLibp2p } = require('../src')
|
||||
const WS = require('libp2p-websockets')
|
||||
const PeerInfo = require('peer-info')
|
||||
|
||||
describe('libp2p creation', () => {
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('should be able to start and stop successfully', (done) => {
|
||||
createNode([], {
|
||||
config: {
|
||||
EXPERIMENTAL: {
|
||||
pubsub: true
|
||||
},
|
||||
dht: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
}, (err, node) => {
|
||||
expect(err).to.not.exist()
|
||||
|
||||
let sw = node._switch
|
||||
let cm = node.connectionManager
|
||||
let dht = node._dht
|
||||
let pub = node._floodSub
|
||||
|
||||
sinon.spy(sw, 'start')
|
||||
sinon.spy(cm, 'start')
|
||||
sinon.spy(dht, 'start')
|
||||
sinon.spy(dht.randomWalk, 'start')
|
||||
sinon.spy(pub, 'start')
|
||||
sinon.spy(sw, 'stop')
|
||||
sinon.spy(cm, 'stop')
|
||||
sinon.spy(dht, 'stop')
|
||||
sinon.spy(dht.randomWalk, 'stop')
|
||||
sinon.spy(pub, 'stop')
|
||||
sinon.spy(node, 'emit')
|
||||
|
||||
series([
|
||||
(cb) => node.start(cb),
|
||||
(cb) => {
|
||||
expect(sw.start.calledOnce).to.equal(true)
|
||||
expect(cm.start.calledOnce).to.equal(true)
|
||||
expect(dht.start.calledOnce).to.equal(true)
|
||||
expect(dht.randomWalk.start.calledOnce).to.equal(true)
|
||||
expect(pub.start.calledOnce).to.equal(true)
|
||||
expect(node.emit.calledWith('start')).to.equal(true)
|
||||
|
||||
cb()
|
||||
},
|
||||
(cb) => node.stop(cb)
|
||||
], (err) => {
|
||||
expect(err).to.not.exist()
|
||||
|
||||
expect(sw.stop.calledOnce).to.equal(true)
|
||||
expect(cm.stop.calledOnce).to.equal(true)
|
||||
expect(dht.stop.calledOnce).to.equal(true)
|
||||
expect(dht.randomWalk.stop.called).to.equal(true)
|
||||
expect(pub.stop.calledOnce).to.equal(true)
|
||||
expect(node.emit.calledWith('stop')).to.equal(true)
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should not create disabled modules', (done) => {
|
||||
createNode([], {
|
||||
config: {
|
||||
EXPERIMENTAL: {
|
||||
pubsub: false
|
||||
}
|
||||
}
|
||||
}, (err, node) => {
|
||||
expect(err).to.not.exist()
|
||||
expect(node._floodSub).to.not.exist()
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not throw errors from switch if node has no error listeners', (done) => {
|
||||
createNode([], {}, (err, node) => {
|
||||
expect(err).to.not.exist()
|
||||
|
||||
node._switch.emit('error', new Error('bad things'))
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should emit errors from switch if node has error listeners', (done) => {
|
||||
const error = new Error('bad things')
|
||||
createNode([], {}, (err, node) => {
|
||||
expect(err).to.not.exist()
|
||||
node.once('error', (err) => {
|
||||
expect(err).to.eql(error)
|
||||
done()
|
||||
})
|
||||
node._switch.emit('error', error)
|
||||
})
|
||||
})
|
||||
|
||||
it('createLibp2p should create a peerInfo instance', function (done) {
|
||||
this.timeout(10e3)
|
||||
createLibp2p({
|
||||
modules: {
|
||||
transport: [ WS ]
|
||||
}
|
||||
}, (err, libp2p) => {
|
||||
expect(err).to.not.exist()
|
||||
expect(libp2p).to.exist()
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('createLibp2p should allow for a provided peerInfo instance', function (done) {
|
||||
this.timeout(10e3)
|
||||
PeerInfo.create((err, peerInfo) => {
|
||||
expect(err).to.not.exist()
|
||||
sinon.spy(PeerInfo, 'create')
|
||||
createLibp2p({
|
||||
peerInfo,
|
||||
modules: {
|
||||
transport: [ WS ]
|
||||
}
|
||||
}, (err, libp2p) => {
|
||||
expect(err).to.not.exist()
|
||||
expect(libp2p).to.exist()
|
||||
expect(PeerInfo.create.callCount).to.eql(0)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
168
test/dht.node.js
168
test/dht.node.js
@ -1,168 +0,0 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
'use strict'
|
||||
|
||||
const chai = require('chai')
|
||||
chai.use(require('dirty-chai'))
|
||||
const expect = chai.expect
|
||||
|
||||
const MemoryStore = require('interface-datastore').MemoryDatastore
|
||||
|
||||
const createNode = require('./utils/create-node')
|
||||
|
||||
describe('.dht', () => {
|
||||
describe('enabled', () => {
|
||||
let nodeA
|
||||
const datastore = new MemoryStore()
|
||||
|
||||
before(function (done) {
|
||||
createNode('/ip4/0.0.0.0/tcp/0', {
|
||||
datastore
|
||||
}, (err, node) => {
|
||||
expect(err).to.not.exist()
|
||||
nodeA = node
|
||||
|
||||
// Rewrite validators
|
||||
nodeA._dht.validators.v = {
|
||||
func (key, publicKey, callback) {
|
||||
setImmediate(callback)
|
||||
},
|
||||
sign: false
|
||||
}
|
||||
|
||||
// Rewrite selectors
|
||||
nodeA._dht.selectors.v = () => 0
|
||||
|
||||
// Start
|
||||
nodeA.start(done)
|
||||
})
|
||||
})
|
||||
|
||||
after((done) => {
|
||||
nodeA.stop(done)
|
||||
})
|
||||
|
||||
it('should be able to dht.put a value to the DHT', (done) => {
|
||||
const key = Buffer.from('key')
|
||||
const value = Buffer.from('value')
|
||||
|
||||
nodeA.dht.put(key, value, (err) => {
|
||||
expect(err).to.not.exist()
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should be able to dht.get a value from the DHT with options', (done) => {
|
||||
const key = Buffer.from('/v/hello')
|
||||
const value = Buffer.from('world')
|
||||
|
||||
nodeA.dht.put(key, value, (err) => {
|
||||
expect(err).to.not.exist()
|
||||
|
||||
nodeA.dht.get(key, { maxTimeout: 3000 }, (err, res) => {
|
||||
expect(err).to.not.exist()
|
||||
expect(res).to.eql(value)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should be able to dht.get a value from the DHT with no options defined', (done) => {
|
||||
const key = Buffer.from('/v/hello')
|
||||
const value = Buffer.from('world')
|
||||
|
||||
nodeA.dht.put(key, value, (err) => {
|
||||
expect(err).to.not.exist()
|
||||
|
||||
nodeA.dht.get(key, (err, res) => {
|
||||
expect(err).to.not.exist()
|
||||
expect(res).to.eql(value)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should be able to dht.getMany a value from the DHT with options', (done) => {
|
||||
const key = Buffer.from('/v/hello')
|
||||
const value = Buffer.from('world')
|
||||
|
||||
nodeA.dht.put(key, value, (err) => {
|
||||
expect(err).to.not.exist()
|
||||
|
||||
nodeA.dht.getMany(key, 1, { maxTimeout: 3000 }, (err, res) => {
|
||||
expect(err).to.not.exist()
|
||||
expect(res).to.exist()
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should be able to dht.getMany a value from the DHT with no options defined', (done) => {
|
||||
const key = Buffer.from('/v/hello')
|
||||
const value = Buffer.from('world')
|
||||
|
||||
nodeA.dht.put(key, value, (err) => {
|
||||
expect(err).to.not.exist()
|
||||
|
||||
nodeA.dht.getMany(key, 1, (err, res) => {
|
||||
expect(err).to.not.exist()
|
||||
expect(res).to.exist()
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('disabled', () => {
|
||||
let nodeA
|
||||
|
||||
before(function (done) {
|
||||
createNode('/ip4/0.0.0.0/tcp/0', {
|
||||
config: {
|
||||
dht: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
}, (err, node) => {
|
||||
expect(err).to.not.exist()
|
||||
nodeA = node
|
||||
nodeA.start(done)
|
||||
})
|
||||
})
|
||||
|
||||
after((done) => {
|
||||
nodeA.stop(done)
|
||||
})
|
||||
|
||||
it('should receive an error on dht.put if the dht is disabled', (done) => {
|
||||
const key = Buffer.from('key')
|
||||
const value = Buffer.from('value')
|
||||
|
||||
nodeA.dht.put(key, value, (err) => {
|
||||
expect(err).to.exist()
|
||||
expect(err.code).to.equal('ERR_DHT_DISABLED')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should receive an error on dht.get if the dht is disabled', (done) => {
|
||||
const key = Buffer.from('key')
|
||||
|
||||
nodeA.dht.get(key, (err) => {
|
||||
expect(err).to.exist()
|
||||
expect(err.code).to.equal('ERR_DHT_DISABLED')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should receive an error on dht.getMany if the dht is disabled', (done) => {
|
||||
const key = Buffer.from('key')
|
||||
|
||||
nodeA.dht.getMany(key, 10, (err) => {
|
||||
expect(err).to.exist()
|
||||
expect(err.code).to.equal('ERR_DHT_DISABLED')
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user