From 6495361c811ce9d0e913af1d5599bf1d57541fb7 Mon Sep 17 00:00:00 2001 From: dmitriy ryajov Date: Tue, 21 Feb 2017 00:19:32 -0800 Subject: [PATCH 01/15] initial commit --- LICENSE | 21 +++++++++++++++++++++ README.md | 1 + 2 files changed, 22 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bbfffbf --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 libp2p + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index fcf2f47..58285da 100644 --- a/README.md +++ b/README.md @@ -1 +1,2 @@ # js-libp2p-circuit +Circuit Switching for libp2p From 67962a5e4eec9e30f799949d55bf287d08beb82b Mon Sep 17 00:00:00 2001 From: dmitriy ryajov Date: Tue, 21 Feb 2017 00:28:36 -0800 Subject: [PATCH 02/15] adding misc files --- .gitignore | 38 ++++++++++++++++++++++++++++++++++++++ .npmignore | 35 +++++++++++++++++++++++++++++++++++ .travis.yml | 38 ++++++++++++++++++++++++++++++++++++++ circle.yml | 12 ++++++++++++ package.json | 45 +++++++++++++++++++++++++++++++++++++++++++++ src/index.js | 0 6 files changed, 168 insertions(+) create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 .travis.yml create mode 100644 circle.yml create mode 100644 package.json create mode 100644 src/index.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c1af64b --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +node_modules + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Vim editor swap files +*.swp + +dist diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..16c0a9b --- /dev/null +++ b/.npmignore @@ -0,0 +1,35 @@ +test + +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +node_modules + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..2957a4b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,38 @@ +sudo: false +language: node_js + +matrix: + include: + - node_js: 4 + env: CXX=g++-4.8 + - node_js: 6 + env: + - SAUCE=true + - CXX=g++-4.8 + - node_js: "stable" + env: + - CXX=g++-4.8 + +# Make sure we have new NPM. +before_install: + - npm install -g npm + +script: + - npm run lint + - npm test + - npm run coverage + +before_script: + - export DISPLAY=:99.0 + - sh -e /etc/init.d/xvfb start + +after_success: + - npm run coverage-publish + +addons: + firefox: latest + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 diff --git a/circle.yml b/circle.yml new file mode 100644 index 0000000..434211a --- /dev/null +++ b/circle.yml @@ -0,0 +1,12 @@ +machine: + node: + version: stable + +dependencies: + pre: + - google-chrome --version + - wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - + - sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' + - sudo apt-get update + - sudo apt-get --only-upgrade install google-chrome-stable + - google-chrome --version diff --git a/package.json b/package.json new file mode 100644 index 0000000..af173e7 --- /dev/null +++ b/package.json @@ -0,0 +1,45 @@ +{ + "name": "libp2p-circuit", + "version": "0.9.2", + "description": "JavaScript implementation of circuit/switch relaying", + "main": "src/index.js", + "scripts": { + "lint": "aegir-lint", + "test": "gulp test", + "test:node": "gulp test:node", + "test:browser": "gulp test:browser", + "build": "gulp build", + "release": "gulp release", + "release-minor": "gulp release --type minor", + "release-major": "gulp release --type major", + "coverage": "gulp coverage", + "coverage-publish": "aegir-coverage publish" + }, + "browser": { + "pull-ws/server": false + }, + "pre-commit": [ + "lint", + "test" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p-websockets.git" + }, + "keywords": [ + "IPFS" + ], + "author": "David Dias ", + "license": "MIT", + "bugs": { + "url": "https://github.com/dryajov/js-libp2p-circuit/issues" + }, + "homepage": "https://github.com/dryajov/js-libp2p-circuit#readme", + "devDependencies": { + "aegir": "^10.0.0", + "pre-commit": "^1.2.2" + }, + "contributors": [ + "Dmitriy Ryajov " + ] +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..e69de29 From 19445c533f3f64d897196b53c2e4beceaa0b489d Mon Sep 17 00:00:00 2001 From: dmitriy ryajov Date: Thu, 2 Mar 2017 10:07:55 -0800 Subject: [PATCH 03/15] feat: initial implementation of circuit relaying --- package.json | 45 ++++++++------- src/config.js | 11 ++++ src/index.js | 6 ++ src/peer.js | 42 ++++++++++++++ src/relay.js | 134 +++++++++++++++++++++++++++++++++++++++++++++ test/index.spec.js | 127 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 346 insertions(+), 19 deletions(-) create mode 100644 src/config.js create mode 100644 src/peer.js create mode 100644 src/relay.js create mode 100644 test/index.spec.js diff --git a/package.json b/package.json index af173e7..def2e50 100644 --- a/package.json +++ b/package.json @@ -1,45 +1,52 @@ { "name": "libp2p-circuit", - "version": "0.9.2", + "version": "0.0.1", "description": "JavaScript implementation of circuit/switch relaying", "main": "src/index.js", "scripts": { "lint": "aegir-lint", - "test": "gulp test", - "test:node": "gulp test:node", - "test:browser": "gulp test:browser", - "build": "gulp build", - "release": "gulp release", - "release-minor": "gulp release --type minor", - "release-major": "gulp release --type major", - "coverage": "gulp coverage", + "build": "aegir-build", + "test": "aegir-test --env node", + "release": "aegir-release", + "release-minor": "aegir-release --type minor", + "release-major": "aegir-release --type major", + "coverage": "aegir-coverage", "coverage-publish": "aegir-coverage publish" }, - "browser": { - "pull-ws/server": false - }, "pre-commit": [ "lint", "test" ], "repository": { "type": "git", - "url": "git+https://github.com/libp2p/js-libp2p-websockets.git" + "url": "git+https://github.com/libp2p/js-libp2p-circuit.git" }, "keywords": [ "IPFS" ], - "author": "David Dias ", + "author": "Dmitriy Ryajov ", "license": "MIT", "bugs": { - "url": "https://github.com/dryajov/js-libp2p-circuit/issues" + "url": "https://github.com/libp2p/js-libp2p-circuit/issues" }, - "homepage": "https://github.com/dryajov/js-libp2p-circuit#readme", + "homepage": "https://github.com/libp2p/js-libp2p-circuit#readme", "devDependencies": { "aegir": "^10.0.0", + "chai": "^3.5.0", + "libp2p-ipfs-nodejs": "^0.19.0", + "peer-id": "^0.8.2", + "peer-info": "^0.8.3", "pre-commit": "^1.2.2" }, - "contributors": [ - "Dmitriy Ryajov " - ] + "contributors": [], + "dependencies": { + "async": "^2.1.5", + "debug": "^2.6.1", + "interface-connection": "^0.3.1", + "lodash": "^4.17.4", + "multiaddr": "^2.2.1", + "multistream-select": "^0.13.4", + "pull-handshake": "^1.1.4", + "pull-stream": "^3.5.0" + } } diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..9d72525 --- /dev/null +++ b/src/config.js @@ -0,0 +1,11 @@ +'use strict' + +const debug = require('debug') + +const log = debug('libp2p:circuit') +log.err = debug('libp2p:circuit:error') + +module.exports = { + log: log, + multicodec: '/ipfs/relay/circuit/1.0.0' +} diff --git a/src/index.js b/src/index.js index e69de29..a61358a 100644 --- a/src/index.js +++ b/src/index.js @@ -0,0 +1,6 @@ +'use strict' + +exports.Dialer = require('./transport/dialer') +exports.Listener = require('./transport/listener') +exports.Peer = require('./peer') +exports.Relay = require('./relay') diff --git a/src/peer.js b/src/peer.js new file mode 100644 index 0000000..3a1792c --- /dev/null +++ b/src/peer.js @@ -0,0 +1,42 @@ +'use strict' + +/** + * The known state of a connected peer. + */ +class Peer { + /** + * @param {Connection} conn + * @param {PeerInfo} peerInfo + */ + constructor (conn, peerInfo) { + /** + * @type {Connection} + */ + this.conn = conn + + /** + * @type {PeerInfo} + */ + this.peerInfo = peerInfo + } + + /** + * Attach a connection + * @param {Connection} conn + * @returns {void} + */ + attachConnection (conn) { + this.conn = conn + } + + /** + * Is the peer connected currently? + * + * @type {boolean} + */ + get isConnected () { + return Boolean(this.conn) + } +} + +module.exports = Peer diff --git a/src/relay.js b/src/relay.js new file mode 100644 index 0000000..e9862c2 --- /dev/null +++ b/src/relay.js @@ -0,0 +1,134 @@ +'use strict' + +const pull = require('pull-stream') +const lp = require('pull-length-prefixed') +const multiaddr = require('multiaddr') +const config = require('./config') +const Peer = require('./peer') +const handshake = require('pull-handshake') +const mss = require('multistream-select') +const Connection = require('interface-connection').Connection + +const multicodec = require('./config').multicodec + +const log = config.log + +class Relay { + constructor (libp2p) { + this.libp2p = libp2p + this.peers = new Map() + + this._onConnection = this._onConnection.bind(this) + this._dialPeer = this._dialPeer.bind(this) + } + + start (cb) { + this.libp2p.handle(multicodec, this._onConnection) + cb() + } + + stop (cb) { + this.libp2p.unhandle(multicodec) + cb() + } + + _dialPeer (ma, callback) { + let idB58Str + + try { + idB58Str = ma.peerId() // try to get the peerid from the multiaddr + } catch (err) { + log.err(err) + } + + if (idB58Str) { + const peer = this.peers.get(idB58Str) + if (peer && peer.isConnected()) { + return + } + } + + this.libp2p.dialByMultiaddr(ma, multicodec, (err, conn) => { + if (err) { + log.err(err) + return callback(err) + } + + conn.getPeerInfo((err, peerInfo) => { + if (err) { + err(err) + return + } + + const idB58Str = peerInfo.id.toB58String() + // If already had a dial to me, just add the conn + if (!this.peers.has(idB58Str)) { + this.peers.set(idB58Str, new Peer(conn, peerInfo)) + } + + const peer = this.peers.get(idB58Str) + callback(null, peer) + }) + }) + } + + _onConnection (protocol, conn) { + conn.getPeerInfo((err, peerInfo) => { + if (err) { + log.err('Failed to identify incomming conn', err) + return pull(pull.empty(), conn) + } + + const idB58Str = peerInfo.id.toB58String() + let srcPeer = this.peers.get(idB58Str) + if (!srcPeer) { + log('new peer', idB58Str) + srcPeer = new Peer(conn, peerInfo) + this.peers.set(idB58Str, srcPeer) + } + this._processConnection(srcPeer, conn) + }) + } + + _processConnection (srcPeer, conn) { + let stream = handshake({timeout: 1000 * 60}) + let shake = stream.handshake + + lp.decodeFromReader(shake, (err, msg) => { + if (err) { + log.err(err) + return pull(pull.empty(), conn) + } + + let addr = multiaddr(msg.toString()) + srcPeer.attachConnection(new Connection(shake.rest(), conn)) + this._circuit(srcPeer, addr) + }) + + pull(stream, conn, stream) + } + + _circuit (srcPeer, ma, callback) { + this._dialPeer(ma, (err, destPeer) => { + if (err) { + log.err(err) + return callback(err) + } + + let srcAddrs = destPeer.peerInfo.distinctMultiaddr() + + if (!(srcAddrs && srcAddrs.length > 0)) { + log.err(`No valid multiaddress for peer!`) + } + + let stream = handshake({timeout: 1000 * 60}, callback) + let shake = stream.handshake + + mss.util.writeEncoded(shake, `${srcAddrs[0].toString()}/ipfs/${srcPeer.peerInfo.id.toB58String()}`) + pull(stream, destPeer.conn, stream) + pull(srcPeer.conn, shake.rest(), srcPeer.conn) + }) + } +} + +module.exports = Relay diff --git a/test/index.spec.js b/test/index.spec.js new file mode 100644 index 0000000..7c056fa --- /dev/null +++ b/test/index.spec.js @@ -0,0 +1,127 @@ +/* eslint-env mocha */ +'use strict' + +const Node = require('libp2p-ipfs-nodejs') +const PeerInfo = require('peer-info') +const series = require('async/series') +const parallel = require('async/parallel') +const pull = require('pull-stream') + +const Relay = require('../src').Relay +const Dialer = require('../src').Dialer +const Listener = require('../src').Listener + +const expect = require('chai').expect + +describe(`test circuit`, () => { + let srcNode + let dstNode + let relayNode + + let srcPeer + let dstPeer + let relayPeer + + let dialer + let relayCircuit + let listener + + let portBase = 9000 // TODO: randomize or mock sockets + before((done) => { + series([ + (cb) => { + PeerInfo.create((err, info) => { + srcPeer = info + srcPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}`) + srcNode = new Node(srcPeer) + cb(err) + }) + }, + (cb) => { + PeerInfo.create((err, info) => { + dstPeer = info + dstPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}`) + dstNode = new Node(dstPeer) + cb(err) + }) + }, + (cb) => { + PeerInfo.create((err, info) => { + relayPeer = info + relayPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}`) + relayNode = new Node(relayPeer) + cb(err) + }) + }, + (cb) => { + let relays = new Map() + relays.set(relayPeer.id.toB58String(), relayPeer) + dialer = new Dialer(srcNode, relays) + cb() + }, + (cb) => { + relayCircuit = new Relay(relayNode) + relayCircuit.start(cb) + }], + (err) => done(err) + ) + }) + + beforeEach((done) => { + parallel([ + (cb) => { + srcNode.start(cb) + }, + (cb) => { + dstNode.start(cb) + }, + (cb) => { + relayNode.start(cb) + } + ], (err) => done(err)) + }) + + afterEach((done) => { + parallel([ + (cb) => { + srcNode.stop(cb) + }, + (cb) => { + dstNode.stop(cb) + }, + (cb) => { + relayNode.stop(cb) + } + ], (err) => done(err)) + }) + + it(`should connect to relay peer`, (done) => { + listener = new Listener(dstNode, (conn) => { + pull( + conn, + pull.map((data) => { + return data.toString().split('').reverse().join('') + }), + conn + ) + }) + + listener.listen(() => { + }) + + dialer.dial(dstPeer, (err, conn) => { + if (err) { + done(err) + } + + pull( + pull.values(['hello']), + conn, + pull.collect((err, data) => { + expect(data[0].toString()).to.equal('olleh') + done(err) + }) + ) + }) + }).timeout(50000) +}) From 97f2ec93f3f684ff065f4506d017842255485dc4 Mon Sep 17 00:00:00 2001 From: dmitriy ryajov Date: Sun, 5 Mar 2017 01:56:19 -0800 Subject: [PATCH 04/15] chore: adding default readme --- .../src/transport/listener_20170305012002.js | 93 ++++++++++++++++ .vscode/settings.json | 3 + README.md | 40 ++++++- src/transport/dialer.js | 104 ++++++++++++++++++ src/transport/listener.js | 90 +++++++++++++++ 5 files changed, 328 insertions(+), 2 deletions(-) create mode 100644 .history/src/transport/listener_20170305012002.js create mode 100644 .vscode/settings.json create mode 100644 src/transport/dialer.js create mode 100644 src/transport/listener.js diff --git a/.history/src/transport/listener_20170305012002.js b/.history/src/transport/listener_20170305012002.js new file mode 100644 index 0000000..22d4e2b --- /dev/null +++ b/.history/src/transport/listener_20170305012002.js @@ -0,0 +1,93 @@ +'use strict' + +const config = require('../config') +const pull = require('pull-stream') +const multiaddr = require('multiaddr') +const PeerInfo = require('peer-info') +const Peer = require('../peer') +const includes = require('lodash/includes') +const lp = require('pull-length-prefixed') +const handshake = require('pull-handshake') +const Connection = require('interface-connection').Connection + +const multicodec = config.multicodec + +const log = config.log + +class Listener { + constructor(libp2p, handler) { + this.libp2p = libp2p + this.peers = new Map() + this.handler = handler + + this._onConnection = this._onConnection.bind(this) + } + + listen(cb) { + cb = cb || function() {} + this.libp2p.handle(multicodec, this._onConnection) + cb() + } + + close(cb) { + cb = cb || function() {} + this.libp2p.unhandle(multicodec) + cb() + } + + _onConnection(protocol, conn) { + conn.getPeerInfo((err, peerInfo) => { + if (err) { + log.err('Failed to identify incomming conn', err) + return pull(pull.empty(), conn) + } + + const idB58Str = peerInfo.id.toB58String() + let relayPeer = this.peers.get(idB58Str) + if (!relayPeer) { + log('new relay peer', idB58Str) + relayPeer = peerInfo + this.peers.set(idB58Str, new Peer(conn, peerInfo)) + } + this._processConnection(relayPeer, conn) + }) + } + + _processConnection(relayPeer, conn) { + let stream = handshake({ timeout: 1000 * 60 }) + let shake = stream.handshake + + lp.decodeFromReader(shake, (err, msg) => { + if (err) { + err(err) + return err + } + + let addr = multiaddr(msg.toString()) + let src + try { + PeerInfo.create(addr.peerId(), (err, peerInfo) => { + if (err) { + log.err(err) + return err + } + + if (includes(addr.protoNames(), 'ipfs')) { + addr = addr.decapsulate('ipfs') + } + + peerInfo.multiaddr.add(addr) + src = peerInfo + this.handler(new Connection(shake.rest(), peerInfo)) + }) + } catch (err) { + log.err(err) + } + }) + + pull(stream, conn, stream) + } + +} + +module.exports = Listener \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..20af2f6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +// Place your settings in this file to overwrite default and user settings. +{ +} \ No newline at end of file diff --git a/README.md b/README.md index 58285da..17244b8 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,38 @@ -# js-libp2p-circuit -Circuit Switching for libp2p +# + +[![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://ipn.io) +[![](https://img.shields.io/badge/project-libp2p-blue.svg?style=flat-square)](http://github.com/libp2p/libp2p) +[![](https://img.shields.io/badge/freenode-%23ipfs-blue.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23ipfs) +[![](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) + +> + + + +## Table of Contents + + + +## Install + + + +## Usage + + + +## Lead + +- [](https://github.com/) + +## Contribute + +Please contribute! [Look at the issues](https://github.com/libp2p//issues)! + +Check out our [contributing document](https://github.com/libp2p/community/blob/master/CONTRIBUTE.md) for more information on how we work, and about contributing in general. Please be aware that all interactions related to libp2p are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). + +Small note: If editing the README, please conform to the [standard-readme](https://github.com/RichardLitt/standard-readme) specification. + +## License + +[MIT](LICENSE) © 2016 Protocol Labs Inc. \ No newline at end of file diff --git a/src/transport/dialer.js b/src/transport/dialer.js new file mode 100644 index 0000000..5db1c5c --- /dev/null +++ b/src/transport/dialer.js @@ -0,0 +1,104 @@ +'use strict' + +const config = require('../config') +const mss = require('multistream-select') +const pull = require('pull-stream') +const handshake = require('pull-handshake') +const Peer = require('../peer') +const Connection = require('interface-connection').Connection + +const log = config.log + +class Dialer { + constructor (libp2p, relayPeers) { + this.libp2p = libp2p + this.relayPeers = relayPeers || new Map() + this.peers = new Map() + + // Speed up any new peer that comes in my way + // this.libp2p.swarm.on('peer-mux-established', this._isRelayPeer) + // this.libp2p.swarm.on('peer-mux-closed', () => { + // // TODO: detach relay connection + // }) + } + + dial (peerInfo, callback) { + if (this.peers.has(peerInfo.id.toB58String())) { + return callback(null, + this.peers.get(peerInfo.id.toB58String()).conn) + } + + let next = (relayPeer) => { + if (!relayPeer) { + return callback(`no relay peers were found!`) + } + + log(`Trying relay peer ${relayPeer.id.toB58String()}`) + this._dialRelay(relayPeer, (err, conn) => { + if (err) { + if (relays.length > 0) { + return next(relays.shift()) + } + return callback(err) + } + + this._negotiateRelay(conn, peerInfo, (err, conn) => { + if (err) { + log.err(`An error has occurred negotiating the relay connection`, err) + return callback(err) + } + + this.peers.set(peerInfo.id.toB58String(), new Peer(conn, peerInfo)) + callback(null, conn) + }) + }) + } + + let relays = Array.from(this.relayPeers.values()) + next(relays.shift()) + } + + _negotiateRelay (conn, peerInfo, callback) { + let src = this.libp2p.peerInfo.distinctMultiaddr() + let dst = peerInfo.distinctMultiaddr() + + if (!(src && src.length > 0) || !(dst && dst.length > 0)) { + log.err(`No valid multiaddress for peer!`) + callback(`No valid multiaddress for peer!`) + } + + let stream = handshake({timeout: 1000 * 60}, callback) + let shake = stream.handshake + + log(`negotiating relay for peer ${peerInfo.id.toB58String()}`) + mss.util.writeEncoded(shake, `${dst[0].toString()}/ipfs/${peerInfo.id.toB58String()}`) + + pull(stream, conn, stream) + callback(null, new Connection(shake.rest(), peerInfo)) + } + + _isRelayPeer (peerInfo) { + this._dialRelay(peerInfo, (peerInfo, conn) => { + // TODO: implement relay peer discovery here + }) + } + + _dialRelay (relayPeer, callback) { + const idB58Str = relayPeer.id.toB58String() + log('dialing %s', idB58Str) + + if (this.peers.has(idB58Str)) { + return callback(null, this.peers.get(idB58Str)) + } + + this.libp2p.dialByPeerInfo(relayPeer, config.multicodec, (err, conn) => { + if (err) { + return callback(err) + } + + callback(null, conn) + }) + } +} + +module.exports = Dialer diff --git a/src/transport/listener.js b/src/transport/listener.js new file mode 100644 index 0000000..27f489f --- /dev/null +++ b/src/transport/listener.js @@ -0,0 +1,90 @@ +'use strict' + +const config = require('../config') +const pull = require('pull-stream') +const multiaddr = require('multiaddr') +const PeerInfo = require('peer-info') +const Peer = require('../peer') +const includes = require('lodash/includes') +const lp = require('pull-length-prefixed') +const handshake = require('pull-handshake') +const Connection = require('interface-connection').Connection + +const multicodec = config.multicodec +const log = config.log + +class Listener { + constructor (libp2p, handler) { + this.libp2p = libp2p + this.peers = new Map() + this.handler = handler + + this._onConnection = this._onConnection.bind(this) + } + + listen (cb) { + cb = cb || function () {} + this.libp2p.handle(multicodec, this._onConnection) + cb() + } + + close (cb) { + cb = cb || function () {} + this.libp2p.unhandle(multicodec) + cb() + } + + _onConnection (protocol, conn) { + conn.getPeerInfo((err, peerInfo) => { + if (err) { + log.err('Failed to identify incomming conn', err) + return pull(pull.empty(), conn) + } + + const idB58Str = peerInfo.id.toB58String() + let relayPeer = this.peers.get(idB58Str) + if (!relayPeer) { + log('new relay peer', idB58Str) + relayPeer = peerInfo + this.peers.set(idB58Str, new Peer(conn, peerInfo)) + } + this._processConnection(relayPeer, conn) + }) + } + + _processConnection (relayPeer, conn) { + let stream = handshake({ timeout: 1000 * 60 }) + let shake = stream.handshake + + lp.decodeFromReader(shake, (err, msg) => { + if (err) { + err(err) + return err + } + + let addr = multiaddr(msg.toString()) + try { + PeerInfo.create(addr.peerId(), (err, peerInfo) => { + if (err) { + log.err(err) + return err + } + + if (includes(addr.protoNames(), 'ipfs')) { + addr = addr.decapsulate('ipfs') + } + + peerInfo.multiaddr.add(addr) + this.handler(new Connection(shake.rest(), peerInfo)) + }) + } catch (err) { + log.err(err) + } + }) + + pull(stream, conn, stream) + } + +} + +module.exports = Listener From 5e5f4b3a9b650723a7401866d08638fc42bbbcda Mon Sep 17 00:00:00 2001 From: dmitriy ryajov Date: Mon, 6 Mar 2017 09:15:03 -0800 Subject: [PATCH 05/15] fix: cleanup --- .gitignore | 2 + package.json | 1 + src/peer.js | 1 - src/relay.js | 24 +++--- src/transport/listener.js | 2 +- test/index.spec.js | 160 +++++++++++++++++++++++--------------- 6 files changed, 115 insertions(+), 75 deletions(-) diff --git a/.gitignore b/.gitignore index c1af64b..7389507 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ node_modules *.swp dist + +.history \ No newline at end of file diff --git a/package.json b/package.json index def2e50..a93759e 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "lodash": "^4.17.4", "multiaddr": "^2.2.1", "multistream-select": "^0.13.4", + "pull-abortable": "^4.1.0", "pull-handshake": "^1.1.4", "pull-stream": "^3.5.0" } diff --git a/src/peer.js b/src/peer.js index 3a1792c..48abc3b 100644 --- a/src/peer.js +++ b/src/peer.js @@ -13,7 +13,6 @@ class Peer { * @type {Connection} */ this.conn = conn - /** * @type {PeerInfo} */ diff --git a/src/relay.js b/src/relay.js index e9862c2..de803cd 100644 --- a/src/relay.js +++ b/src/relay.js @@ -6,8 +6,9 @@ const multiaddr = require('multiaddr') const config = require('./config') const Peer = require('./peer') const handshake = require('pull-handshake') -const mss = require('multistream-select') const Connection = require('interface-connection').Connection +const RelaySession = require('./relay-session') +const utils = require('./utils') const multicodec = require('./config').multicodec @@ -17,17 +18,20 @@ class Relay { constructor (libp2p) { this.libp2p = libp2p this.peers = new Map() + this.relaySessions = new Map() this._onConnection = this._onConnection.bind(this) this._dialPeer = this._dialPeer.bind(this) } start (cb) { + cb = cb || function () {} this.libp2p.handle(multicodec, this._onConnection) cb() } stop (cb) { + cb = cb || function () {} this.libp2p.unhandle(multicodec) cb() } @@ -115,18 +119,14 @@ class Relay { return callback(err) } - let srcAddrs = destPeer.peerInfo.distinctMultiaddr() + let relaySession = new RelaySession(srcPeer, destPeer) + this.relaySessions.set( + utils.getCircuitStrAddr(srcPeer.peerInfo, destPeer.peerInfo), + relaySession + ) - if (!(srcAddrs && srcAddrs.length > 0)) { - log.err(`No valid multiaddress for peer!`) - } - - let stream = handshake({timeout: 1000 * 60}, callback) - let shake = stream.handshake - - mss.util.writeEncoded(shake, `${srcAddrs[0].toString()}/ipfs/${srcPeer.peerInfo.id.toB58String()}`) - pull(stream, destPeer.conn, stream) - pull(srcPeer.conn, shake.rest(), srcPeer.conn) + // create the circuit + relaySession.circuit() }) } } diff --git a/src/transport/listener.js b/src/transport/listener.js index 27f489f..b3784a6 100644 --- a/src/transport/listener.js +++ b/src/transport/listener.js @@ -64,7 +64,7 @@ class Listener { let addr = multiaddr(msg.toString()) try { - PeerInfo.create(addr.peerId(), (err, peerInfo) => { + PeerInfo.create(addr.getPeerId(), (err, peerInfo) => { if (err) { log.err(err) return err diff --git a/test/index.spec.js b/test/index.spec.js index 7c056fa..3e44078 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -4,7 +4,6 @@ const Node = require('libp2p-ipfs-nodejs') const PeerInfo = require('peer-info') const series = require('async/series') -const parallel = require('async/parallel') const pull = require('pull-stream') const Relay = require('../src').Relay @@ -13,7 +12,7 @@ const Listener = require('../src').Listener const expect = require('chai').expect -describe(`test circuit`, () => { +describe(`test circuit`, function () { let srcNode let dstNode let relayNode @@ -28,6 +27,8 @@ describe(`test circuit`, () => { let portBase = 9000 // TODO: randomize or mock sockets before((done) => { + this.timeout(50000) + series([ (cb) => { PeerInfo.create((err, info) => { @@ -52,76 +53,113 @@ describe(`test circuit`, () => { relayNode = new Node(relayPeer) cb(err) }) - }, - (cb) => { - let relays = new Map() - relays.set(relayPeer.id.toB58String(), relayPeer) - dialer = new Dialer(srcNode, relays) - cb() - }, - (cb) => { - relayCircuit = new Relay(relayNode) - relayCircuit.start(cb) }], (err) => done(err) ) }) - beforeEach((done) => { - parallel([ - (cb) => { - srcNode.start(cb) - }, - (cb) => { - dstNode.start(cb) - }, - (cb) => { - relayNode.start(cb) - } - ], (err) => done(err)) - }) - - afterEach((done) => { - parallel([ - (cb) => { - srcNode.stop(cb) - }, - (cb) => { - dstNode.stop(cb) - }, - (cb) => { - relayNode.stop(cb) - } - ], (err) => done(err)) - }) + describe(`simple circuit tests`, function circuitTests () { + beforeEach(function (done) { + series([ + (cb) => { + srcNode.start(cb) + }, + (cb) => { + dstNode.start(cb) + }, + (cb) => { + relayNode.start(cb) + }, + (cb) => { + let relays = new Map() + relays.set(relayPeer.id.toB58String(), relayPeer) + dialer = new Dialer(srcNode, relays) + cb() + }, + (cb) => { + relayCircuit = new Relay(relayNode) + relayCircuit.start(cb) + } + ], (err) => done(err)) + }) - it(`should connect to relay peer`, (done) => { - listener = new Listener(dstNode, (conn) => { - pull( - conn, - pull.map((data) => { - return data.toString().split('').reverse().join('') - }), - conn - ) + afterEach(function circuitTests (done) { + series([ + (cb) => { + srcNode.stop(cb) + }, + (cb) => { + dstNode.stop(cb) + }, + (cb) => { + relayNode.stop(cb) + }, + (cb) => { + relayCircuit.stop() + relayCircuit = null + dialer = null + cb() + } + ], (err) => done(err)) }) - listener.listen(() => { + it(`source should be able to write/read to dest over relay`, function (done) { + listener = new Listener(dstNode, (conn) => { + pull( + conn, + pull.map((data) => { + return data.toString().split('').reverse().join('') + }), + conn + ) + }) + + listener.listen() + + dialer.dial(dstPeer, (err, conn) => { + if (err) { + done(err) + } + + pull( + pull.values(['hello']), + conn, + pull.collect((err, data) => { + expect(data[0].toString()).to.equal('olleh') + listener.close() + done(err) + }) + ) + }) }) - dialer.dial(dstPeer, (err, conn) => { - if (err) { - done(err) - } + it(`destination should be able to write/read to source over relay`, function (done) { + listener = new Listener(dstNode, (conn) => { + pull( + pull.values(['hello']), + conn, + pull.collect((err, data) => { + expect(data[0].toString()).to.equal('olleh') + listener.close() + done(err) + }) + ) + }) - pull( - pull.values(['hello']), - conn, - pull.collect((err, data) => { - expect(data[0].toString()).to.equal('olleh') + listener.listen() + + dialer.dial(dstPeer, (err, conn) => { + if (err) { done(err) - }) - ) + } + pull( + conn, + pull.map((data) => { + return data.toString().split('').reverse().join('') + }), + conn + ) + }) }) - }).timeout(50000) + }) }) From 4ca90ba017de2bbe60b27295a67932c384b0c00c Mon Sep 17 00:00:00 2001 From: dmitriy ryajov Date: Fri, 10 Mar 2017 11:27:30 -0800 Subject: [PATCH 06/15] feat: adding relay session and utils --- .gitignore | 3 +- src/relay-session.js | 89 ++++++++++++++++++++++++++++++++++++++++++++ src/utils.js | 33 ++++++++++++++++ test/index.spec.js | 56 ++++++++++++++-------------- 4 files changed, 152 insertions(+), 29 deletions(-) create mode 100644 src/relay-session.js create mode 100644 src/utils.js diff --git a/.gitignore b/.gitignore index 7389507..1dd9938 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,5 @@ node_modules dist -.history \ No newline at end of file +.history +.vscode diff --git a/src/relay-session.js b/src/relay-session.js new file mode 100644 index 0000000..34c5378 --- /dev/null +++ b/src/relay-session.js @@ -0,0 +1,89 @@ +'use strict' + +const pull = require('pull-stream') +const handshake = require('pull-handshake') +const mss = require('multistream-select') +const config = require('./config') +const abortable = require('pull-abortable') +const utils = require('./utils') + +const log = config.log + +class RelaySession { + /** + * Constructs a relay RelaySession + * @param {Peer} src - the source peer + * @param {Peer} dst - the destination peer + */ + constructor (src, dst) { + this.src = src + this.dst = dst + this.srcStrAddr = null + this.dstStrAddr = null + this.circuitAddr = null + this.stream = abortable() + this.isActive = false + } + + /** + * is relay active + * @returns {Boolean} is stream active + */ + isActive () { + return this.isActive + } + + /** + * Circuit two connections + * @returns {void} + */ + circuit () { + let stream = handshake({timeout: 1000 * 60}, (err) => { + log.err(err) + this.stream.abort() + }) + + let shake = stream.handshake + + mss.util.writeEncoded( + shake, + utils.getDstAddrAsString(this.dst.peerInfo) + ) + + // create handshake stream + pull( + stream, + this.dst.conn, + stream + ) + + // circuit the src and dst streams + pull( + this.src.conn, + shake.rest(), + this.stream, + this.src.conn + ) + + this.isActive = true + } + + /** + * Closes the open connection to peer + * + * @param {Function} callback + * @returns {undefined} + */ + close (callback) { + // abort the circuit stream + if (this.stream) { + this.stream.abort() + } + setImmediate(() => { + this.stream = null + callback() + }) + } +} + +module.exports = RelaySession diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..4dc6f31 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,33 @@ +'use strict' +const config = require('./config') + +const log = config.log + +function getDstAddrAsString (peerInfo) { + return getStrAddress(peerInfo) +} + +function getSrcAddrAsString (peerInfo) { + return getStrAddress(peerInfo) +} + +function getCircuitStrAddr (srcPeer, dstPeer) { + return `${getSrcAddrAsString(srcPeer)}/p2p-circuit/${getDstAddrAsString(dstPeer)}` +} + +function getStrAddress (peerInfo) { + let addrs = peerInfo.distinctMultiaddr() + + if (!(addrs && addrs.length > 0)) { + log.err(`No valid multiaddress for peer!`) + return null + } + + // pick the first address from the available multiaddrs for now + return `${addrs[0].toString()}/ipfs/${peerInfo.id.toB58String()}` +} + +exports.getDstAddrAsString = getDstAddrAsString +exports.getSrcAddrAsString = getSrcAddrAsString +exports.getCircuitStrAddr = getCircuitStrAddr +exports.getStrAddress = getStrAddress diff --git a/test/index.spec.js b/test/index.spec.js index 3e44078..e9d5edc 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -133,33 +133,33 @@ describe(`test circuit`, function () { }) }) - it(`destination should be able to write/read to source over relay`, function (done) { - listener = new Listener(dstNode, (conn) => { - pull( - pull.values(['hello']), - conn, - pull.collect((err, data) => { - expect(data[0].toString()).to.equal('olleh') - listener.close() - done(err) - }) - ) - }) - - listener.listen() - - dialer.dial(dstPeer, (err, conn) => { - if (err) { - done(err) - } - pull( - conn, - pull.map((data) => { - return data.toString().split('').reverse().join('') - }), - conn - ) - }) - }) + // it(`destination should be able to write/read to source over relay`, function (done) { + // listener = new Listener(dstNode, (conn) => { + // pull( + // pull.values(['hello']), + // conn, + // pull.collect((err, data) => { + // expect(data[0].toString()).to.equal('olleh') + // listener.close() + // done(err) + // }) + // ) + // }) + + // listener.listen() + + // dialer.dial(dstPeer, (err, conn) => { + // if (err) { + // done(err) + // } + // pull( + // conn, + // pull.map((data) => { + // return data.toString().split('').reverse().join('') + // }), + // conn + // ) + // }) + // }) }) }) From 509305e1b3ded3921b0fc0880348f720802cfc3e Mon Sep 17 00:00:00 2001 From: dmitriy ryajov Date: Sat, 11 Mar 2017 01:28:03 -0800 Subject: [PATCH 07/15] feat: reworking as a transport --- package.json | 13 ++- src/index.js | 6 +- src/peer.js | 5 + src/relay-session.js | 6 +- src/relay.js | 30 +++-- src/transport/dialer.js | 104 ----------------- src/transport/index.js | 234 ++++++++++++++++++++++++++++++++++++++ src/transport/listener.js | 62 ++++++---- test/index.spec.js | 194 +++++++++++++++---------------- 9 files changed, 418 insertions(+), 236 deletions(-) delete mode 100644 src/transport/dialer.js create mode 100644 src/transport/index.js diff --git a/package.json b/package.json index a93759e..8c4c295 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,17 @@ "devDependencies": { "aegir": "^10.0.0", "chai": "^3.5.0", + "libp2p-ipfs-browser": "^0.19.0", "libp2p-ipfs-nodejs": "^0.19.0", + "libp2p-mdns": "^0.6.1", + "libp2p-multiplex": "^0.4.1", + "libp2p-railing": "^0.4.1", + "libp2p-secio": "^0.6.7", + "libp2p-spdy": "^0.10.4", + "libp2p-swarm": "^0.26.18", + "libp2p-tcp": "^0.9.3", + "libp2p-webrtc-star": "^0.8.8", + "libp2p-websockets": "^0.9.2", "peer-id": "^0.8.2", "peer-info": "^0.8.3", "pre-commit": "^1.2.2" @@ -48,6 +58,7 @@ "multistream-select": "^0.13.4", "pull-abortable": "^4.1.0", "pull-handshake": "^1.1.4", - "pull-stream": "^3.5.0" + "pull-stream": "^3.5.0", + "setimmediate": "^1.0.5" } } diff --git a/src/index.js b/src/index.js index a61358a..d87e768 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,6 @@ 'use strict' -exports.Dialer = require('./transport/dialer') +exports = module.exports = require('./relay') +exports.Circuit = require('./transport') exports.Listener = require('./transport/listener') -exports.Peer = require('./peer') -exports.Relay = require('./relay') + diff --git a/src/peer.js b/src/peer.js index 48abc3b..d961ff9 100644 --- a/src/peer.js +++ b/src/peer.js @@ -36,6 +36,11 @@ class Peer { get isConnected () { return Boolean(this.conn) } + + close () { + this.conn = null + this.peerInfo = null + } } module.exports = Peer diff --git a/src/relay-session.js b/src/relay-session.js index 34c5378..e747b52 100644 --- a/src/relay-session.js +++ b/src/relay-session.js @@ -6,12 +6,14 @@ const mss = require('multistream-select') const config = require('./config') const abortable = require('pull-abortable') const utils = require('./utils') +const Connection = require('interface-connection').Connection const log = config.log class RelaySession { /** * Constructs a relay RelaySession + * * @param {Peer} src - the source peer * @param {Peer} dst - the destination peer */ @@ -27,6 +29,7 @@ class RelaySession { /** * is relay active + * * @returns {Boolean} is stream active */ isActive () { @@ -35,6 +38,7 @@ class RelaySession { /** * Circuit two connections + * * @returns {void} */ circuit () { @@ -60,8 +64,8 @@ class RelaySession { // circuit the src and dst streams pull( this.src.conn, - shake.rest(), this.stream, + shake.rest(), this.src.conn ) diff --git a/src/relay.js b/src/relay.js index de803cd..f6b362b 100644 --- a/src/relay.js +++ b/src/relay.js @@ -21,7 +21,6 @@ class Relay { this.relaySessions = new Map() this._onConnection = this._onConnection.bind(this) - this._dialPeer = this._dialPeer.bind(this) } start (cb) { @@ -40,7 +39,7 @@ class Relay { let idB58Str try { - idB58Str = ma.peerId() // try to get the peerid from the multiaddr + idB58Str = ma.getPeerId() // try to get the peerid from the multiaddr } catch (err) { log.err(err) } @@ -52,7 +51,12 @@ class Relay { } } - this.libp2p.dialByMultiaddr(ma, multicodec, (err, conn) => { + let addr = ma.toString() + if (addr.startsWith('/p2p-circuit')) { + addr = addr.substring(String('/p2p-circuit').length) + } + + this.libp2p.dialByMultiaddr(multiaddr(addr), multicodec, (err, conn) => { if (err) { log.err(err) return callback(err) @@ -68,10 +72,11 @@ class Relay { // If already had a dial to me, just add the conn if (!this.peers.has(idB58Str)) { this.peers.set(idB58Str, new Peer(conn, peerInfo)) + } else { + this.peers.get(idB58Str).attachConnection(conn) } - const peer = this.peers.get(idB58Str) - callback(null, peer) + callback(null, this.peers.get(idB58Str)) }) }) } @@ -101,15 +106,21 @@ class Relay { lp.decodeFromReader(shake, (err, msg) => { if (err) { log.err(err) - return pull(pull.empty(), conn) + return } let addr = multiaddr(msg.toString()) - srcPeer.attachConnection(new Connection(shake.rest(), conn)) - this._circuit(srcPeer, addr) + let newConn = new Connection(shake.rest(), conn) + srcPeer.attachConnection(newConn) + this._circuit(srcPeer, addr, (err) => { + if (err) { + log.err(err) + return + } + }) }) - pull(stream, conn, stream) + return pull(stream, conn, stream) } _circuit (srcPeer, ma, callback) { @@ -127,6 +138,7 @@ class Relay { // create the circuit relaySession.circuit() + return callback() }) } } diff --git a/src/transport/dialer.js b/src/transport/dialer.js deleted file mode 100644 index 5db1c5c..0000000 --- a/src/transport/dialer.js +++ /dev/null @@ -1,104 +0,0 @@ -'use strict' - -const config = require('../config') -const mss = require('multistream-select') -const pull = require('pull-stream') -const handshake = require('pull-handshake') -const Peer = require('../peer') -const Connection = require('interface-connection').Connection - -const log = config.log - -class Dialer { - constructor (libp2p, relayPeers) { - this.libp2p = libp2p - this.relayPeers = relayPeers || new Map() - this.peers = new Map() - - // Speed up any new peer that comes in my way - // this.libp2p.swarm.on('peer-mux-established', this._isRelayPeer) - // this.libp2p.swarm.on('peer-mux-closed', () => { - // // TODO: detach relay connection - // }) - } - - dial (peerInfo, callback) { - if (this.peers.has(peerInfo.id.toB58String())) { - return callback(null, - this.peers.get(peerInfo.id.toB58String()).conn) - } - - let next = (relayPeer) => { - if (!relayPeer) { - return callback(`no relay peers were found!`) - } - - log(`Trying relay peer ${relayPeer.id.toB58String()}`) - this._dialRelay(relayPeer, (err, conn) => { - if (err) { - if (relays.length > 0) { - return next(relays.shift()) - } - return callback(err) - } - - this._negotiateRelay(conn, peerInfo, (err, conn) => { - if (err) { - log.err(`An error has occurred negotiating the relay connection`, err) - return callback(err) - } - - this.peers.set(peerInfo.id.toB58String(), new Peer(conn, peerInfo)) - callback(null, conn) - }) - }) - } - - let relays = Array.from(this.relayPeers.values()) - next(relays.shift()) - } - - _negotiateRelay (conn, peerInfo, callback) { - let src = this.libp2p.peerInfo.distinctMultiaddr() - let dst = peerInfo.distinctMultiaddr() - - if (!(src && src.length > 0) || !(dst && dst.length > 0)) { - log.err(`No valid multiaddress for peer!`) - callback(`No valid multiaddress for peer!`) - } - - let stream = handshake({timeout: 1000 * 60}, callback) - let shake = stream.handshake - - log(`negotiating relay for peer ${peerInfo.id.toB58String()}`) - mss.util.writeEncoded(shake, `${dst[0].toString()}/ipfs/${peerInfo.id.toB58String()}`) - - pull(stream, conn, stream) - callback(null, new Connection(shake.rest(), peerInfo)) - } - - _isRelayPeer (peerInfo) { - this._dialRelay(peerInfo, (peerInfo, conn) => { - // TODO: implement relay peer discovery here - }) - } - - _dialRelay (relayPeer, callback) { - const idB58Str = relayPeer.id.toB58String() - log('dialing %s', idB58Str) - - if (this.peers.has(idB58Str)) { - return callback(null, this.peers.get(idB58Str)) - } - - this.libp2p.dialByPeerInfo(relayPeer, config.multicodec, (err, conn) => { - if (err) { - return callback(err) - } - - callback(null, conn) - }) - } -} - -module.exports = Dialer diff --git a/src/transport/index.js b/src/transport/index.js new file mode 100644 index 0000000..4308d99 --- /dev/null +++ b/src/transport/index.js @@ -0,0 +1,234 @@ +'use strict' + +const config = require('../config') +const mss = require('multistream-select') +const pull = require('pull-stream') +const handshake = require('pull-handshake') +const Peer = require('../peer') +const Connection = require('interface-connection').Connection +const mafmt = require('mafmt') +const PeerInfo = require('peer-info') +const isFunction = require('lodash.isfunction') +const EventEmmiter = require('events').EventEmitter +const multiaddr = require('multiaddr') +const includes = require('lodash/includes') +const lp = require('pull-length-prefixed') + +const log = config.log + +class Circuit { + /** + * Creates an instance of Dialer. + * @param {Swarm} swarm - the swarm + * + * @memberOf Dialer + */ + constructor (swarm) { + this.swarm = swarm + this.relayPeers = new Map() + this.peers = new Map() + + this.swarm.on('peer-mux-established', this._addRelayPeer.bind(this)) + this.swarm.on('peer-mux-closed', (peerInfo) => { + this.relayPeers.delete(peerInfo.id.toB58String()) + }) + } + + /** + * Dial a peer over a relay + * + * @param {multiaddr} ma - the multiaddr of the peer to dial + * @param {Object} options - dial options + * @param {Function} cb - a callback called once dialed + * @returns {void} + * + * @memberOf Dialer + */ + dial (ma, options, cb) { + if (isFunction(options)) { + cb = options + options = {} + } + + if (!cb) { + cb = () => { + } + } + + let idB58Str + + ma = multiaddr(ma) + try { + idB58Str = ma.getPeerId() // try to get the peerid from the multiaddr + } catch (err) { + log.err(err) + cb(err) + } + + if (this.peers.has(idB58Str)) { + return cb(null, + this.peers.get(idB58Str).conn) + } + + PeerInfo.create(idB58Str, (err, dstPeer) => { + if (err) { + log.err(err) + cb(err) + } + + dstPeer.multiaddr.add(ma) + + let relays = Array.from(this.relayPeers.values()) + let next = (relayPeer) => { + if (!relayPeer) { + return cb(`no relay peers were found!`) + } + + log(`Trying relay peer ${relayPeer.peerInfo.id.toB58String()}`) + this._dialRelay(relayPeer.peerInfo, (err, conn) => { + if (err) { + if (relays.length > 0) { + return next(relays.shift()) + } + return cb(err) + } + + this._negotiateRelay(conn, dstPeer, (err, conn) => { + if (err) { + log.err(`An error has occurred negotiating the relay connection`, err) + return cb(err) + } + + let stream = handshake({timeout: 1000 * 60}, cb) + let shake = stream.handshake + + lp.decodeFromReader(shake, (err, msg) => { + if (err) { + return cb(err) + } + + if (msg.toString() === 'ok') { + cb(null, new Connection(shake.rest(), conn)) + } else { + return cb(new Error(`ko`)) + } + }) + + pull(stream, conn, stream) + this.peers.set(idB58Str, new Peer(conn, dstPeer)) + }) + }) + } + + next(relays.shift()) + }) + } + + createListener (options, handler) { + if (isFunction(options)) { + handler = options + options = {} + } + + handler = handler || (() => { + }) + + return new EventEmmiter() + } + + /** + * Negotiate the relay connection + * + * @param {Connection} conn - a connection to the relay + * @param {PeerInfo} peerInfo - the peerInfo of the peer to relay the connection for + * @param {Callback} callback - a callback with that return the negotiated relay connection + * @returns {void} + * + * @memberOf Dialer + */ + _negotiateRelay (conn, peerInfo, callback) { + let src = this.swarm._peerInfo.distinctMultiaddr() + let dst = peerInfo.distinctMultiaddr() + + if (!(src && src.length > 0) || !(dst && dst.length > 0)) { + log.err(`No valid multiaddress for peer!`) + callback(`No valid multiaddress for peer!`) + } + + let stream = handshake({timeout: 1000 * 60}, callback) + let shake = stream.handshake + + log(`negotiating relay for peer ${peerInfo.id}`) + mss.util.writeEncoded(shake, dst[0].toString()) + + pull(stream, conn, stream) + callback(null, new Connection(shake.rest(), conn)) + } + + /** + * Connect to a relay peer + * + * @param {PeerInfo} peerInfo - the PeerInfo of the relay + * @returns {void} + * + * @memberOf Dialer + */ + _addRelayPeer (peerInfo) { + if (!this.relayPeers.has(peerInfo.id.toB58String())) { + for (let ma of peerInfo.multiaddrs) { + if (mafmt.Circuit.matches(ma)) { + let peer = new Peer(null, peerInfo) + this.relayPeers.set(peerInfo.id.toB58String(), peer) + + // attempt to dia the relay so that we have a connection + this._dialRelay(peerInfo, (err, conn) => { + if (err) { + log.err(err) + return + } + peer.attachConnection(conn) + }) + break + } + } + } + } + + /** + * Dial a relay peer by its PeerInfo + * + * @param {PeerInfo} relayPeer - the PeerInfo of the relay peer + * @param {Function} callback - a callback with the connection to the relay peer + * @returns {void} + * + * @memberOf Dialer + */ + _dialRelay (relayPeer, callback) { + const idB58Str = relayPeer.id.toB58String() + log('dialing %s', idB58Str) + + if (this.peers.has(idB58Str)) { + return callback(null, this.peers.get(idB58Str)) + } + + this.swarm.dial(relayPeer, config.multicodec, (err, conn) => { + if (err) { + return callback(err) + } + + callback(null, conn) + }) + } + + filter (multiaddrs) { + if (!Array.isArray(multiaddrs)) { + multiaddrs = [multiaddrs] + } + return multiaddrs.filter((ma) => { + return mafmt.Circuit.matches(ma) + }) + } + +} + +module.exports = Circuit diff --git a/src/transport/listener.js b/src/transport/listener.js index b3784a6..f4f9721 100644 --- a/src/transport/listener.js +++ b/src/transport/listener.js @@ -5,32 +5,43 @@ const pull = require('pull-stream') const multiaddr = require('multiaddr') const PeerInfo = require('peer-info') const Peer = require('../peer') -const includes = require('lodash/includes') const lp = require('pull-length-prefixed') const handshake = require('pull-handshake') const Connection = require('interface-connection').Connection +const EventEmitter = require('events').EventEmitter +const mss = require('multistream-select') const multicodec = config.multicodec const log = config.log -class Listener { - constructor (libp2p, handler) { - this.libp2p = libp2p +class Listener extends EventEmitter { + constructor (swarm) { + super() + this.swarm = swarm this.peers = new Map() - this.handler = handler this._onConnection = this._onConnection.bind(this) } listen (cb) { - cb = cb || function () {} - this.libp2p.handle(multicodec, this._onConnection) + cb = cb || (() => { + }) + this.swarm.handle(multicodec, this._onConnection) + this.emit('listening') cb() } - close (cb) { - cb = cb || function () {} - this.libp2p.unhandle(multicodec) + close (options, cb) { + if (typeof options === 'function') { + cb = options + options = {} + } + cb = cb || (() => { + }) + options = options || {} + + this.swarm.unhandle(multicodec) + this.emit('close') cb() } @@ -38,6 +49,7 @@ class Listener { conn.getPeerInfo((err, peerInfo) => { if (err) { log.err('Failed to identify incomming conn', err) + this.emit('error', err) return pull(pull.empty(), conn) } @@ -48,18 +60,22 @@ class Listener { relayPeer = peerInfo this.peers.set(idB58Str, new Peer(conn, peerInfo)) } - this._processConnection(relayPeer, conn) + this._processConnection(relayPeer, conn, (err) => { + if (err) { + log.err(err) + this.emit('error', new Error(err)) + } + }) }) } - _processConnection (relayPeer, conn) { + _processConnection (relayPeer, conn, cb) { let stream = handshake({ timeout: 1000 * 60 }) let shake = stream.handshake lp.decodeFromReader(shake, (err, msg) => { if (err) { - err(err) - return err + return cb(err) } let addr = multiaddr(msg.toString()) @@ -67,24 +83,26 @@ class Listener { PeerInfo.create(addr.getPeerId(), (err, peerInfo) => { if (err) { log.err(err) - return err - } - - if (includes(addr.protoNames(), 'ipfs')) { - addr = addr.decapsulate('ipfs') + this.handler(err) } + mss.util.writeEncoded(shake, 'ok') peerInfo.multiaddr.add(addr) - this.handler(new Connection(shake.rest(), peerInfo)) + const conn = new Connection(shake.rest(), peerInfo) + this.emit('connection', conn) }) } catch (err) { - log.err(err) + cb(err) } }) pull(stream, conn, stream) } - } module.exports = Listener +module.exports.listener = (swarm) => { + const listener = new Listener(swarm) + listener.listen() + return listener +} diff --git a/test/index.spec.js b/test/index.spec.js index e9d5edc..382f981 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -2,16 +2,51 @@ 'use strict' const Node = require('libp2p-ipfs-nodejs') +const BrowserNode = require('libp2p-ipfs-browser') const PeerInfo = require('peer-info') const series = require('async/series') const pull = require('pull-stream') - -const Relay = require('../src').Relay -const Dialer = require('../src').Dialer -const Listener = require('../src').Listener +const Libp2p = require('libp2p') +const multistream = require('multistream-select') +const Connection = require('interface-connection').Connection +const multiaddr = require('multiaddr') + +const TCP = require('libp2p-tcp') +const WebRTCStar = require('libp2p-webrtc-star') +const MulticastDNS = require('libp2p-mdns') +const WS = require('libp2p-websockets') +const Railing = require('libp2p-railing') +const spdy = require('libp2p-spdy') +const multiplex = require('libp2p-multiplex') +const secio = require('libp2p-secio') const expect = require('chai').expect +process.on('uncaughtException', function (err) { + console.log(err) +}) + +class TestNode extends Libp2p { + constructor (peerInfo, transports, options) { + options = options || {} + + const modules = { + transport: transports, + connection: { + muxer: [ + spdy + // multiplex + ], + crypto: [ + // secio + ] + }, + discovery: [] + } + super(modules, peerInfo, null, options) + } +} + describe(`test circuit`, function () { let srcNode let dstNode @@ -21,90 +56,72 @@ describe(`test circuit`, function () { let dstPeer let relayPeer - let dialer - let relayCircuit - let listener - let portBase = 9000 // TODO: randomize or mock sockets before((done) => { - this.timeout(50000) - series([ (cb) => { PeerInfo.create((err, info) => { - srcPeer = info - srcPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}`) - srcNode = new Node(srcPeer) + relayPeer = info + relayPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}`) + relayPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}/ws`) + relayPeer.multiaddr.add(`/p2p-circuit`) + relayNode = new TestNode(relayPeer, [new TCP(), new WS()], {relay: true}) cb(err) }) }, (cb) => { PeerInfo.create((err, info) => { - dstPeer = info - dstPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}`) - dstNode = new Node(dstPeer) + srcPeer = info + srcPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}/ws`) + srcNode = new TestNode(srcPeer, [new WS()]) + srcNode.peerBook.put(relayPeer) cb(err) }) }, (cb) => { PeerInfo.create((err, info) => { - relayPeer = info - relayPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}`) - relayNode = new Node(relayPeer) + dstPeer = info + dstPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}`) + dstNode = new TestNode(dstPeer, [new TCP()]) + srcNode.peerBook.put(relayPeer) cb(err) }) - }], - (err) => done(err) + } + ], (err) => done(err) ) }) describe(`simple circuit tests`, function circuitTests () { beforeEach(function (done) { series([ - (cb) => { - srcNode.start(cb) - }, - (cb) => { - dstNode.start(cb) - }, (cb) => { relayNode.start(cb) }, (cb) => { - let relays = new Map() - relays.set(relayPeer.id.toB58String(), relayPeer) - dialer = new Dialer(srcNode, relays) - cb() + srcNode.start(cb) }, (cb) => { - relayCircuit = new Relay(relayNode) - relayCircuit.start(cb) + dstNode.start(cb) } ], (err) => done(err)) }) - afterEach(function circuitTests (done) { - series([ - (cb) => { - srcNode.stop(cb) - }, - (cb) => { - dstNode.stop(cb) - }, - (cb) => { - relayNode.stop(cb) - }, - (cb) => { - relayCircuit.stop() - relayCircuit = null - dialer = null - cb() - } - ], (err) => done(err)) - }) + // afterEach(function circuitTests (done) { + // series([ + // (cb) => { + // srcNode.stop(cb) + // }, + // (cb) => { + // dstNode.stop(cb) + // }, + // (cb) => { + // relayNode.stop(cb) + // } + // ], (err) => done(err)) + // }) - it(`source should be able to write/read to dest over relay`, function (done) { - listener = new Listener(dstNode, (conn) => { + it(`should connect to relay`, function (done) { + function hello (protocol, conn) { pull( conn, pull.map((data) => { @@ -112,54 +129,39 @@ describe(`test circuit`, function () { }), conn ) - }) + } - listener.listen() - - dialer.dial(dstPeer, (err, conn) => { + srcNode.dialByPeerInfo(relayPeer, (err) => { if (err) { done(err) } - pull( - pull.values(['hello']), - conn, - pull.collect((err, data) => { - expect(data[0].toString()).to.equal('olleh') - listener.close() - done(err) + // srcNode.swarm.emit('peer-mux-established', relayPeer) + srcNode.handle('/ipfs/reverse/1.0.0', hello) + + dstNode.dialByPeerInfo(relayPeer, (err) => { + if (err) { + return done(err) + } + + // dstNode.swarm.emit('peer-mux-established', relayPeer) + + dstNode.dialByPeerInfo(srcPeer, '/ipfs/reverse/1.0.0', (err, conn) => { + if (err) { + return done(err) + } + + pull( + pull.values([new Buffer('hello')]), + conn, + pull.collect((err, data) => { + expect(data[0].toString()).to.equal('olleh') + done(err) + }) + ) }) - ) + }) }) }) - - // it(`destination should be able to write/read to source over relay`, function (done) { - // listener = new Listener(dstNode, (conn) => { - // pull( - // pull.values(['hello']), - // conn, - // pull.collect((err, data) => { - // expect(data[0].toString()).to.equal('olleh') - // listener.close() - // done(err) - // }) - // ) - // }) - - // listener.listen() - - // dialer.dial(dstPeer, (err, conn) => { - // if (err) { - // done(err) - // } - // pull( - // conn, - // pull.map((data) => { - // return data.toString().split('').reverse().join('') - // }), - // conn - // ) - // }) - // }) }) -}) +}).timeout(500000) From d0e37ebaf0673fc00b56f86f6709cd4e209d99d5 Mon Sep 17 00:00:00 2001 From: dmitriy ryajov Date: Wed, 15 Mar 2017 22:26:55 -0700 Subject: [PATCH 08/15] feat: getting peers communicating over relay (wip) --- package.json | 2 +- src/index.js | 1 - src/relay-session.js | 1 - src/relay.js | 37 +-- src/transport/index.js | 27 +- src/transport/listener.js | 10 +- test/index.spec.js | 522 +++++++++++++++++++++++++++++++------- 7 files changed, 469 insertions(+), 131 deletions(-) diff --git a/package.json b/package.json index 8c4c295..554dd26 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "libp2p-circuit", - "version": "0.0.1", + "version": "0.0.1-alpha.1", "description": "JavaScript implementation of circuit/switch relaying", "main": "src/index.js", "scripts": { diff --git a/src/index.js b/src/index.js index d87e768..5c7004b 100644 --- a/src/index.js +++ b/src/index.js @@ -3,4 +3,3 @@ exports = module.exports = require('./relay') exports.Circuit = require('./transport') exports.Listener = require('./transport/listener') - diff --git a/src/relay-session.js b/src/relay-session.js index e747b52..74cfd6b 100644 --- a/src/relay-session.js +++ b/src/relay-session.js @@ -6,7 +6,6 @@ const mss = require('multistream-select') const config = require('./config') const abortable = require('pull-abortable') const utils = require('./utils') -const Connection = require('interface-connection').Connection const log = config.log diff --git a/src/relay.js b/src/relay.js index f6b362b..eaa95b7 100644 --- a/src/relay.js +++ b/src/relay.js @@ -3,16 +3,17 @@ const pull = require('pull-stream') const lp = require('pull-length-prefixed') const multiaddr = require('multiaddr') -const config = require('./config') const Peer = require('./peer') const handshake = require('pull-handshake') const Connection = require('interface-connection').Connection const RelaySession = require('./relay-session') const utils = require('./utils') +const debug = require('debug') const multicodec = require('./config').multicodec -const log = config.log +const log = debug('libp2p:circuit:relay') +log.err = debug('libp2p:circuit:error:relay') class Relay { constructor (libp2p) { @@ -36,20 +37,20 @@ class Relay { } _dialPeer (ma, callback) { - let idB58Str + // let idB58Str - try { - idB58Str = ma.getPeerId() // try to get the peerid from the multiaddr - } catch (err) { - log.err(err) - } + // try { + // idB58Str = ma.getPeerId() // try to get the peerid from the multiaddr + // } catch (err) { + // log.err(err) + // } - if (idB58Str) { - const peer = this.peers.get(idB58Str) - if (peer && peer.isConnected()) { - return - } - } + // if (idB58Str) { + // const peer = this.peers.get(idB58Str) + // if (peer && peer.isConnected) { + // return callback(null, peer) + // } + // } let addr = ma.toString() if (addr.startsWith('/p2p-circuit')) { @@ -82,9 +83,9 @@ class Relay { } _onConnection (protocol, conn) { - conn.getPeerInfo((err, peerInfo) => { + setImmediate(() => conn.getPeerInfo((err, peerInfo) => { if (err) { - log.err('Failed to identify incomming conn', err) + log.err('Failed to identify incoming conn', err) return pull(pull.empty(), conn) } @@ -96,7 +97,7 @@ class Relay { this.peers.set(idB58Str, srcPeer) } this._processConnection(srcPeer, conn) - }) + })) } _processConnection (srcPeer, conn) { @@ -115,7 +116,7 @@ class Relay { this._circuit(srcPeer, addr, (err) => { if (err) { log.err(err) - return + return null } }) }) diff --git a/src/transport/index.js b/src/transport/index.js index 4308d99..5b62db3 100644 --- a/src/transport/index.js +++ b/src/transport/index.js @@ -9,12 +9,13 @@ const Connection = require('interface-connection').Connection const mafmt = require('mafmt') const PeerInfo = require('peer-info') const isFunction = require('lodash.isfunction') -const EventEmmiter = require('events').EventEmitter const multiaddr = require('multiaddr') -const includes = require('lodash/includes') const lp = require('pull-length-prefixed') -const log = config.log +const debug = require('debug') + +const log = debug('libp2p:circuit:dialer') +log.err = debug('libp2p:circuit:error:dialer') class Circuit { /** @@ -40,7 +41,7 @@ class Circuit { * @param {multiaddr} ma - the multiaddr of the peer to dial * @param {Object} options - dial options * @param {Function} cb - a callback called once dialed - * @returns {void} + * @returns {Connection} - the connection * * @memberOf Dialer */ @@ -70,12 +71,14 @@ class Circuit { this.peers.get(idB58Str).conn) } + let dstConn = new Connection() PeerInfo.create(idB58Str, (err, dstPeer) => { if (err) { log.err(err) cb(err) } + dstConn.setPeerInfo(dstPeer) dstPeer.multiaddr.add(ma) let relays = Array.from(this.relayPeers.values()) @@ -108,7 +111,8 @@ class Circuit { } if (msg.toString() === 'ok') { - cb(null, new Connection(shake.rest(), conn)) + dstConn.setInnerConn(shake.rest()) + cb(null, dstConn) } else { return cb(new Error(`ko`)) } @@ -122,18 +126,12 @@ class Circuit { next(relays.shift()) }) + + return dstConn } createListener (options, handler) { - if (isFunction(options)) { - handler = options - options = {} - } - - handler = handler || (() => { - }) - - return new EventEmmiter() + throw new Error(`not implemented`) } /** @@ -228,7 +226,6 @@ class Circuit { return mafmt.Circuit.matches(ma) }) } - } module.exports = Circuit diff --git a/src/transport/listener.js b/src/transport/listener.js index f4f9721..48e1247 100644 --- a/src/transport/listener.js +++ b/src/transport/listener.js @@ -11,8 +11,12 @@ const Connection = require('interface-connection').Connection const EventEmitter = require('events').EventEmitter const mss = require('multistream-select') +const debug = require('debug') + const multicodec = config.multicodec -const log = config.log + +const log = debug('libp2p:circuit:listener') +log.err = debug('libp2p:circuit:error:listener') class Listener extends EventEmitter { constructor (swarm) { @@ -48,7 +52,7 @@ class Listener extends EventEmitter { _onConnection (protocol, conn) { conn.getPeerInfo((err, peerInfo) => { if (err) { - log.err('Failed to identify incomming conn', err) + log.err('Failed to identify incoming conn', err) this.emit('error', err) return pull(pull.empty(), conn) } @@ -70,7 +74,7 @@ class Listener extends EventEmitter { } _processConnection (relayPeer, conn, cb) { - let stream = handshake({ timeout: 1000 * 60 }) + let stream = handshake({timeout: 1000 * 60}) let shake = stream.handshake lp.decodeFromReader(shake, (err, msg) => { diff --git a/test/index.spec.js b/test/index.spec.js index 382f981..5bab492 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -1,24 +1,15 @@ /* eslint-env mocha */ 'use strict' -const Node = require('libp2p-ipfs-nodejs') -const BrowserNode = require('libp2p-ipfs-browser') const PeerInfo = require('peer-info') const series = require('async/series') const pull = require('pull-stream') const Libp2p = require('libp2p') -const multistream = require('multistream-select') -const Connection = require('interface-connection').Connection -const multiaddr = require('multiaddr') const TCP = require('libp2p-tcp') -const WebRTCStar = require('libp2p-webrtc-star') -const MulticastDNS = require('libp2p-mdns') const WS = require('libp2p-websockets') -const Railing = require('libp2p-railing') const spdy = require('libp2p-spdy') -const multiplex = require('libp2p-multiplex') -const secio = require('libp2p-secio') +const waterfall = require('async/waterfall') const expect = require('chai').expect @@ -47,51 +38,353 @@ class TestNode extends Libp2p { } } -describe(`test circuit`, function () { - let srcNode - let dstNode - let relayNode - - let srcPeer - let dstPeer - let relayPeer - - let portBase = 9000 // TODO: randomize or mock sockets - before((done) => { - series([ - (cb) => { - PeerInfo.create((err, info) => { - relayPeer = info - relayPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}`) - relayPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}/ws`) - relayPeer.multiaddr.add(`/p2p-circuit`) - relayNode = new TestNode(relayPeer, [new TCP(), new WS()], {relay: true}) - cb(err) - }) - }, - (cb) => { - PeerInfo.create((err, info) => { - srcPeer = info - srcPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}/ws`) - srcNode = new TestNode(srcPeer, [new WS()]) - srcNode.peerBook.put(relayPeer) - cb(err) +describe('circuit', function () { + describe('test common transports', function () { + let srcNode + let dstNode + let relayNode + + let srcPeer + let dstPeer + let relayPeer + + let portBase = 9000 // TODO: randomize or mock sockets + before((done) => { + series([ + (cb) => { + PeerInfo.create((err, info) => { + relayPeer = info + relayPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}`) + relayPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}/ws`) + relayNode = new TestNode(relayPeer, [new TCP(), new WS()]) + cb(err) + }) + }, + (cb) => { + PeerInfo.create((err, info) => { + srcPeer = info + srcPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}`) + srcNode = new TestNode(srcPeer, [new TCP()]) + srcNode.peerBook.put(relayPeer) + cb(err) + }) + }, + (cb) => { + PeerInfo.create((err, info) => { + dstPeer = info + dstPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}`) + dstNode = new TestNode(dstPeer, [new TCP()]) + srcNode.peerBook.put(relayPeer) + cb(err) + }) + } + ], (err) => done(err) + ) + }) + + beforeEach(function (done) { + series([ + (cb) => { + relayNode.start(cb) + }, + (cb) => { + srcNode.start(cb) + }, + (cb) => { + dstNode.start(cb) + } + ], (err) => done(err)) + }) + + afterEach(function circuitTests (done) { + series([ + (cb) => { + srcNode.stop(cb) + }, + (cb) => { + dstNode.stop(cb) + }, + (cb) => { + relayNode.stop(cb) + } + ], (err) => done(err)) + }) + + it('should dial from source to dest over common transports', function (done) { + this.timeout(500000) + + const handleTestProto = (proto, conn) => { + conn.getPeerInfo((err, peerInfo) => { + if (err) { + return done(err) + } + peerInfo.multiaddrs.forEach((ma) => { + console.log(ma.toString()) + }) + + pull(pull.values(['hello']), conn) }) - }, - (cb) => { - PeerInfo.create((err, info) => { - dstPeer = info - dstPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}`) - dstNode = new TestNode(dstPeer, [new TCP()]) - srcNode.peerBook.put(relayPeer) - cb(err) + } + + srcNode.handle('/ipfs/test/1.0.0', handleTestProto) + dstNode.handle('/ipfs/test/1.0.0', handleTestProto) + + waterfall([ + (cb) => srcNode.dialByPeerInfo(dstPeer, () => { + cb() + }), + (cb) => dstNode.dialByPeerInfo(srcPeer, () => { + cb() + }), + (cb) => { + waterfall([ + (cb) => { + srcNode.dialByPeerInfo(dstPeer, '/ipfs/test/1.0.0', (err, conn) => { + if (err) return cb(err) + + conn.getPeerInfo((err, peerInfo) => { + if (err) return cb(err) + + console.log(`from srcNode to relayNode:`) + peerInfo.multiaddrs.forEach((ma) => { + console.log(`src: ma of relay`, ma.toString()) + }) + + pull(conn, pull.collect((err, data) => { + if (err) return cb(err) + + data.forEach((dta) => { + console.log(dta.toString()) + }) + cb() + })) + }) + }) + }, + (cb) => { + dstNode.dialByPeerInfo(srcPeer, '/ipfs/test/1.0.0', (err, conn) => { + if (err) return cb(err) + + conn.getPeerInfo((err, peerInfo) => { + if (err) return cb(err) + + console.log(`from dstNode to relayNode:`) + peerInfo.multiaddrs.forEach((ma) => { + console.log(`dst: ma of relay`, ma.toString()) + }) + + pull(conn, pull.collect((err, data) => { + if (err) return cb(err) + + data.forEach((dta) => { + console.log(dta.toString()) + }) + cb() + })) + }) + }) + }], done) + } + ]) + }) + }) + + describe('test non common transports', function () { + let srcNode + let dstNode + let relayNode + + let srcPeer + let dstPeer + let relayPeer + + let portBase = 9000 // TODO: randomize or mock sockets + before((done) => { + series([ + (cb) => { + PeerInfo.create((err, info) => { + relayPeer = info + relayPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}`) + relayPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}/ws`) + relayNode = new TestNode(relayPeer, [new TCP(), new WS()]) + cb(err) + }) + }, + (cb) => { + PeerInfo.create((err, info) => { + srcPeer = info + srcPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}`) + srcNode = new TestNode(srcPeer, [new TCP()]) + srcNode.peerBook.put(relayPeer) + cb(err) + }) + }, + (cb) => { + PeerInfo.create((err, info) => { + dstPeer = info + dstPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}/ws`) + dstNode = new TestNode(dstPeer, [new WS()]) + srcNode.peerBook.put(relayPeer) + cb(err) + }) + } + ], (err) => done(err) + ) + }) + + beforeEach(function (done) { + series([ + (cb) => { + relayNode.start(cb) + }, + (cb) => { + srcNode.start(cb) + }, + (cb) => { + dstNode.start(cb) + } + ], (err) => done(err)) + }) + + afterEach(function circuitTests (done) { + series([ + (cb) => { + srcNode.stop(cb) + }, + (cb) => { + dstNode.stop(cb) + }, + (cb) => { + relayNode.stop(cb) + } + ], (err) => done(err)) + }) + + it('should dial to a common node over different transports', function (done) { + this.timeout(500000) + + const handleTestProto = (proto, conn) => { + conn.getPeerInfo((err, peerInfo) => { + if (err) { + return done(err) + } + + console.log(`handling test proto:`) + peerInfo.multiaddrs.forEach((ma) => { + console.log(ma.toString()) + }) + + pull(pull.values(['hello']), conn) }) } - ], (err) => done(err) - ) + + relayNode.handle('/ipfs/test/1.0.0', handleTestProto) + waterfall([ + (cb) => srcNode.dialByPeerInfo(relayPeer, () => { + cb() + }), + (cb) => dstNode.dialByPeerInfo(relayPeer, () => { + cb() + }), + (cb) => { + waterfall([ + // TODO: make sure the WebSockets node runs first, because TCP hangs the stream!!! possibly a bug.... + (cb) => { + dstNode.dialByPeerInfo(relayPeer, '/ipfs/test/1.0.0', (err, conn) => { + if (err) return cb(err) + + conn.getPeerInfo((err, peerInfo) => { + if (err) return cb(err) + + console.log(`from dstNode to relayNode:`) + peerInfo.multiaddrs.forEach((ma) => { + console.log(`dst: ma of relay`, ma.toString()) + }) + + pull(conn, pull.collect((err, data) => { + if (err) return cb(err) + + data.forEach((dta) => { + console.log(dta.toString()) + }) + cb() + })) + }) + }) + }, + (cb) => { + srcNode.dialByPeerInfo(relayPeer, '/ipfs/test/1.0.0', (err, conn) => { + if (err) return cb(err) + + conn.getPeerInfo((err, peerInfo) => { + if (err) return cb(err) + + console.log(`from srcNode to relayNode:`) + peerInfo.multiaddrs.forEach((ma) => { + console.log(`src: ma of relay`, ma.toString()) + }) + + pull(conn, pull.collect((err, data) => { + if (err) return cb(err) + + data.forEach((dta) => { + console.log(dta.toString()) + }) + cb() + })) + }) + }) + } + ], done) + } + ]) + }) }) - describe(`simple circuit tests`, function circuitTests () { + describe('test non common transports over relay', function () { + let srcNode + let dstNode + let relayNode + + let srcPeer + let dstPeer + let relayPeer + + let portBase = 9000 // TODO: randomize or mock sockets + before((done) => { + series([ + (cb) => { + PeerInfo.create((err, info) => { + relayPeer = info + relayPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}`) + relayPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}/ws`) + relayPeer.multiaddr.add(`/p2p-circuit`) + relayNode = new TestNode(relayPeer, [new TCP(), new WS()], {relay: true}) + cb(err) + }) + }, + (cb) => { + PeerInfo.create((err, info) => { + srcPeer = info + srcPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}`) + srcNode = new TestNode(srcPeer, [new TCP()]) + srcNode.peerBook.put(relayPeer) + cb(err) + }) + }, + (cb) => { + PeerInfo.create((err, info) => { + dstPeer = info + dstPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}/ws`) + dstNode = new TestNode(dstPeer, [new WS()]) + srcNode.peerBook.put(relayPeer) + cb(err) + }) + } + ], (err) => done(err) + ) + }) + beforeEach(function (done) { series([ (cb) => { @@ -106,22 +399,24 @@ describe(`test circuit`, function () { ], (err) => done(err)) }) - // afterEach(function circuitTests (done) { - // series([ - // (cb) => { - // srcNode.stop(cb) - // }, - // (cb) => { - // dstNode.stop(cb) - // }, - // (cb) => { - // relayNode.stop(cb) - // } - // ], (err) => done(err)) - // }) - - it(`should connect to relay`, function (done) { - function hello (protocol, conn) { + afterEach(function circuitTests (done) { + series([ + (cb) => { + srcNode.stop(cb) + }, + (cb) => { + dstNode.stop(cb) + }, + (cb) => { + relayNode.stop(cb) + } + ], (err) => done(err)) + }) + + it('should dial to a node over a relay and write a value', function (done) { + this.timeout(500000) + + function reverse (protocol, conn) { pull( conn, pull.map((data) => { @@ -131,37 +426,80 @@ describe(`test circuit`, function () { ) } - srcNode.dialByPeerInfo(relayPeer, (err) => { - if (err) { - done(err) - } + srcNode.handle('/ipfs/reverse/1.0.0', reverse) + waterfall([ + (cb) => srcNode.dialByPeerInfo(relayPeer, () => { + cb() + }), + (cb) => dstNode.dialByPeerInfo(relayPeer, () => { + cb() + }), + (cb) => { + waterfall([ + // TODO: make sure the WebSockets dials first, because TCP hangs the stream!!! possibly a bug in TCP.... + (cb) => { + dstNode.dialByPeerInfo(srcPeer, '/ipfs/reverse/1.0.0', (err, conn) => { + if (err) return cb(err) + pull( + pull.values(['hello']), + conn, + pull.collect((err, data) => { + if (err) return cb(err) - // srcNode.swarm.emit('peer-mux-established', relayPeer) - srcNode.handle('/ipfs/reverse/1.0.0', hello) + expect(data[0].toString()).to.equal('olleh') + cb() + })) + }) + } + ], done) + } + ]) + }) - dstNode.dialByPeerInfo(relayPeer, (err) => { - if (err) { - return done(err) - } + it('should dial to a node over a relay and write several value', function (done) { + this.timeout(500000) - // dstNode.swarm.emit('peer-mux-established', relayPeer) + function reverse (protocol, conn) { + pull( + conn, + pull.map((data) => { + return data.toString().split('').reverse().join('') + }), + conn + ) + } - dstNode.dialByPeerInfo(srcPeer, '/ipfs/reverse/1.0.0', (err, conn) => { - if (err) { - return done(err) - } + srcNode.handle('/ipfs/reverse/1.0.0', reverse) + waterfall([ + (cb) => srcNode.dialByPeerInfo(relayPeer, () => { + cb() + }), + (cb) => dstNode.dialByPeerInfo(relayPeer, () => { + cb() + }), + (cb) => { + waterfall([ + // TODO: make sure the WebSockets dials first, because TCP hangs the stream!!! possibly a bug in TCP.... + (cb) => { + dstNode.dialByPeerInfo(srcPeer, '/ipfs/reverse/1.0.0', (err, conn) => { + if (err) return cb(err) + pull( + pull.values(['hello', 'hello1', 'hello2', 'hello3']), + conn, + pull.collect((err, data) => { + if (err) return cb(err) - pull( - pull.values([new Buffer('hello')]), - conn, - pull.collect((err, data) => { - expect(data[0].toString()).to.equal('olleh') - done(err) + expect(data[0].toString()).to.equal('olleh') + expect(data[1].toString()).to.equal('1olleh') + expect(data[2].toString()).to.equal('2olleh') + expect(data[3].toString()).to.equal('3olleh') + cb() + })) }) - ) - }) - }) - }) + } + ], done) + } + ]) }) }) -}).timeout(500000) +}) From 994bcf11725a544bad0f3a1b1612b43663bfea0d Mon Sep 17 00:00:00 2001 From: Dmitriy Ryajov Date: Fri, 24 Mar 2017 18:00:30 -0700 Subject: [PATCH 09/15] adding this to keep track of requirements/ideas --- implementation-notes.md | 128 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 implementation-notes.md diff --git a/implementation-notes.md b/implementation-notes.md new file mode 100644 index 0000000..9dac075 --- /dev/null +++ b/implementation-notes.md @@ -0,0 +1,128 @@ + + +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 + + From aa0a160b1d3e91bd5550224b689959925cf1d3f6 Mon Sep 17 00:00:00 2001 From: dmitriy ryajov Date: Sat, 25 Mar 2017 01:47:09 -0700 Subject: [PATCH 10/15] feat: rework as a transport --- implementation-notes.md | 1 - src/circuit.js | 119 +++++++++ src/config.js | 11 - src/dialer.js | 266 ++++++++++++++++++++ src/index.js | 10 +- src/listener.js | 87 +++++++ src/multicodec.js | 1 + src/relay-session.js | 92 ------- src/relay.js | 147 ----------- src/transport/index.js | 231 ----------------- src/transport/listener.js | 112 --------- src/utils.js | 5 +- test/index.spec.js | 505 +++++++------------------------------- 13 files changed, 564 insertions(+), 1023 deletions(-) create mode 100644 src/circuit.js delete mode 100644 src/config.js create mode 100644 src/dialer.js create mode 100644 src/listener.js create mode 100644 src/multicodec.js delete mode 100644 src/relay-session.js delete mode 100644 src/relay.js delete mode 100644 src/transport/index.js delete mode 100644 src/transport/listener.js diff --git a/implementation-notes.md b/implementation-notes.md index 9dac075..7684b0a 100644 --- a/implementation-notes.md +++ b/implementation-notes.md @@ -124,5 +124,4 @@ In order to minimize nesting, the first example is preferred - 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 - diff --git a/src/circuit.js b/src/circuit.js new file mode 100644 index 0000000..74ad2f5 --- /dev/null +++ b/src/circuit.js @@ -0,0 +1,119 @@ +'use strict' + +const pull = require('pull-stream') +const lp = require('pull-length-prefixed') +const multiaddr = require('multiaddr') +const Peer = require('./peer') +const handshake = require('pull-handshake') +const utils = require('./utils') +const debug = require('debug') +const includes = require('lodash/includes') +const PeerInfo = require('peer-info') +const PeerId = require('peer-id') + +const multicodec = require('./multicodec') + +const log = debug('libp2p:circuit:circuit') +log.err = debug('libp2p:circuit:error:circuit') + +class Circuit { + + /** + * Construct a Circuit object + * + * This class will handle incoming circuit connections and + * either start a relay or hand the relayed connection to + * the swarm + * + * @param {Swarm} swarm - the swarm this circuit is attched to + */ + constructor (swarm) { + this.swarm = swarm + this.peers = new Map() + this.relaySessions = new Map() + + this.handler = this.handler.bind(this) + } + + /** + * + * @param conn + * @param peerInfo + * @param dstAddr + * @param cb + */ + handler (conn, dstAddr, cb) { + this._circuit(conn, dstAddr, cb) + } + + _dialPeer (ma, callback) { + const peerInfo = new PeerInfo(PeerId.createFromB58String(ma.getPeerId())) + peerInfo.multiaddr.add(ma) + this.swarm.dial(peerInfo, multicodec, (err, conn) => { + if (err) { + log.err(err) + return callback(err) + } + + conn.getPeerInfo((err, peerInfo) => { + if (err) { + err(err) + return + } + + const idB58Str = peerInfo.id.toB58String() + // If already had a dial to me, just add the conn + if (!this.peers.has(idB58Str)) { + this.peers.set(idB58Str, new Peer(conn, peerInfo)) + } else { + this.peers.get(idB58Str).attachConnection(conn) + } + + callback(null, this.peers.get(idB58Str).conn) + }) + }) + } + + _circuit (srcConn, dstMa, cb) { + this._dialPeer(dstMa, (err, dstConn) => { + if (err) { + log.err(err) + return cb(err) + } + + let stream = handshake({timeout: 1000 * 60}, cb) + let shake = stream.handshake + + dstConn.getPeerInfo((err, peerInfo) => { + pull( + pull.values([new Buffer(`/ipfs/${peerInfo.id.toB58String()}`)]), + lp.encode(), + pull.collect((err, encoded) => { + if (err) { + return cb(err) + } + + shake.write(encoded[0]) + // circuit the src and dst streams + pull( + srcConn, + shake.rest(), + srcConn + ) + cb() + }) + ) + }) + + // create handshake stream + pull( + stream, + dstConn, + stream + ) + + }) + } +} + +module.exports = Circuit \ No newline at end of file diff --git a/src/config.js b/src/config.js deleted file mode 100644 index 9d72525..0000000 --- a/src/config.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict' - -const debug = require('debug') - -const log = debug('libp2p:circuit') -log.err = debug('libp2p:circuit:error') - -module.exports = { - log: log, - multicodec: '/ipfs/relay/circuit/1.0.0' -} diff --git a/src/dialer.js b/src/dialer.js new file mode 100644 index 0000000..6b6e667 --- /dev/null +++ b/src/dialer.js @@ -0,0 +1,266 @@ +'use strict' + +const pull = require('pull-stream') +const handshake = require('pull-handshake') +const Peer = require('./peer') +const Connection = require('interface-connection').Connection +const mafmt = require('mafmt') +const PeerInfo = require('peer-info') +const isFunction = require('lodash.isfunction') +const multiaddr = require('multiaddr') +const lp = require('pull-length-prefixed') +const debug = require('debug') + +const log = debug('libp2p:circuit:dialer') +log.err = debug('libp2p:circuit:error:dialer') + +const multicodec = require('./multicodec') + +const createListener = require('./listener') + +class Dialer { + /** + * Creates an instance of Dialer. + * @param {Swarm} swarm - the swarm + * + * @memberOf Dialer + */ + constructor (swarm) { + this.swarm = swarm + this.relayPeers = new Map() + + this.swarm.on('peer-mux-established', this._addRelayPeer.bind(this)) + this.swarm.on('peer-mux-closed', (peerInfo) => { + this.relayPeers.delete(peerInfo.id.toB58String()) + }) + } + + /** + * Dial a peer over a relay + * + * @param {multiaddr} ma - the multiaddr of the peer to dial + * @param {Object} options - dial options + * @param {Function} cb - a callback called once dialed + * @returns {Connection} - the connection + * + * @memberOf Dialer + */ + dial (ma, options, cb) { + if (isFunction(options)) { + cb = options + options = {} + } + + if (!cb) { + cb = () => { + } + } + + let idB58Str + ma = multiaddr(ma) + idB58Str = ma.getPeerId() // try to get the peerId from the multiaddr + if (!idB58Str) { + let err = 'No valid peer id in multiaddr' + log.err(err) + cb(err) + } + + let dstConn = new Connection() + PeerInfo.create(idB58Str, (err, dstPeer) => { + if (err) { + log.err(err) + cb(err) + } + + dstConn.setPeerInfo(dstPeer) + dstPeer.multiaddr.add(ma) + this._initiateRelay(dstPeer, (err, conn) => { + if (err) { + log.err(err) + return dstConn.setInnerConn(pull.empty()) + } + + dstConn.setInnerConn(conn) + cb(null, dstConn) + }) + }) + + return dstConn + } + + /** + * Initate the relay connection + * + * @param {PeerInfo} dstPeer - the destination peer + * @param {Function} cb - callback to call with relayed connection or error + * @returns {void} + * + * @memberOf Dialer + */ + _initiateRelay (dstPeer, cb) { + let relays = Array.from(this.relayPeers.values()) + let next = (relayPeer) => { + if (!relayPeer) { + const err = `no relay peers were found!` + log.err(err) + return cb(err) + } + + log(`Trying relay peer ${relayPeer.peerInfo.id.toB58String()}`) + this._dialRelay(relayPeer.peerInfo, (err, conn) => { + if (err) { + if (relays.length > 0) { + return next(relays.shift()) + } + return cb(err) + } + + this._negotiateRelay(conn, dstPeer, (err, conn) => { + if (err) { + log.err(`An error has occurred negotiating the relay connection`, err) + return cb(err) + } + + return cb(null, conn) + }) + }) + } + + next(relays.shift()) + } + + /** + * Create listener + * + * @param {any} options + * @param {any} handler + * @returns {Listener} + * + * @memberOf Dialer + */ + createListener (handler) { + return createListener(this.swarm, handler) + } + + /** + * Negotiate the relay connection + * + * @param {Connection} conn - a connection to the relay + * @param {PeerInfo} peerInfo - the peerInfo of the peer to relay the connection for + * @param {Function} cb - a callback with that return the negotiated relay connection + * @returns {void} + * + * @memberOf Dialer + */ + _negotiateRelay (conn, peerInfo, cb) { + let src = this.swarm._peerInfo.distinctMultiaddr() + let dst = peerInfo.distinctMultiaddr() + + if (!(src && src.length > 0) || !(dst && dst.length > 0)) { + let err = `No valid multiaddress for peer!` + log.err(err) + cb(err) + } + + let stream = handshake({timeout: 1000 * 60}, cb) + let shake = stream.handshake + + log(`negotiating relay for peer ${peerInfo.id}`) + + const values = [new Buffer(dst[0].toString())] + + pull( + pull.values(values), + lp.encode(), + pull.collect((err, encoded) => { + if (err) { + return cb(err) + } + + shake.write(encoded[0]) + shake.read(1, (err, data) => { + if (err) { + log.err(err) + return cb(err) + } + + cb(null, shake.rest()) + }) + }) + ) + + pull(stream, conn, stream) + } + + /** + * Dial a relay peer by its PeerInfo + * + * @param {PeerInfo} relayPeer - the PeerInfo of the relay peer + * @param {Function} callback - a callback with the connection to the relay peer + * @returns {Function|void} + * + * @memberOf Dialer + */ + _dialRelay (relayPeer, callback) { + const idB58Str = relayPeer.id.toB58String() + log('dialing relay %s', idB58Str) + + this.swarm.dial(relayPeer, multicodec, (err, conn) => { + if (err) { + return callback(err) + } + + callback(null, conn) + }) + } + + /** + * Connect to a relay peer + * + * @param {PeerInfo} peerInfo - the PeerInfo of the relay + * @returns {void} + * + * @memberOf Dialer + */ + _addRelayPeer (peerInfo) { + // TODO: ask connected peers for all their connected peers + // as well and try to establish a relay to them as well + + // TODO: ask peers if they can proactively dial on your behalf to other peers (active/passive) + // should it be a multistream header? + + if (!this.relayPeers.has(peerInfo.id.toB58String())) { + let peer = new Peer(null, peerInfo) + this.relayPeers.set(peerInfo.id.toB58String(), peer) + + // attempt to dia the relay so that we have a connection + this._dialRelay(peerInfo, (err, conn) => { + if (err) { + log.err(err) + return + } + peer.attachConnection(conn) + }) + } + } + + /** + * Filter check for all multiaddresses + * that this transport can dial on + * + * @param {any} multiaddrs + * @returns {Array} + * + * @memberOf Dialer + */ + filter (multiaddrs) { + if (!Array.isArray(multiaddrs)) { + multiaddrs = [multiaddrs] + } + return multiaddrs.filter((ma) => { + return mafmt.Circuit.matches(ma) + }) + } +} + +module.exports = Dialer diff --git a/src/index.js b/src/index.js index 5c7004b..e0f1017 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,5 @@ -'use strict' - -exports = module.exports = require('./relay') -exports.Circuit = require('./transport') -exports.Listener = require('./transport/listener') +module.exports = { + Circuit: require('./circuit'), + Dialer: require('./dialer'), + multicodec: require('./multicodec') +} \ No newline at end of file diff --git a/src/listener.js b/src/listener.js new file mode 100644 index 0000000..de832d2 --- /dev/null +++ b/src/listener.js @@ -0,0 +1,87 @@ +'use strict' + +const includes = require('lodash/includes') +const pull = require('pull-stream') +const Circuit = require('./circuit') +const multicodec = require('./multicodec') +const EE = require('events').EventEmitter +const lp = require('pull-length-prefixed') +const multiaddr = require('multiaddr') +const handshake = require('pull-handshake') +const Connection = require('interface-connection').Connection + +const debug = require('debug') + +const log = debug('libp2p:circuit:listener') +log.err = debug('libp2p:circuit:error:listener') + +module.exports = (swarm, handler) => { + const listener = new EE() + const circuit = new Circuit(swarm) + + listener.listen = (ma, cb) => { + cb = cb || (() => {}) + + swarm.handle(multicodec, (proto, conn) => { + conn.getPeerInfo((err, peerInfo) => { + if (err) { + log.err('Failed to identify incoming conn', err) + return cb(err, null) + } + + let stream = handshake({timeout: 1000 * 60}) + let shake = stream.handshake + + lp.decodeFromReader(shake, (err, msg) => { + if (err) { + log.err(err) + return + } + + let addr = multiaddr(msg.toString()) + // make a circuit + if (includes(addr.protoNames(), 'p2p-circuit')) { + circuit.handler(shake.rest(), addr, (err) => { + if (err) { + log.err(err) + listener.emit('error', err) + return handler(err) + } + + listener.emit('circuit') + return handler() + }) + } else { + // we need this to signal the circuit that the connection is ready + // otherwise, it seems be get circuited prematurely, which causes the + // dialer to fail on a non ready connection + shake.write('\n') + let newConn = new Connection(shake.rest(), conn) + listener.emit('connection', newConn) + handler(null, newConn) + } + }) + + pull(stream, conn, stream) + }) + }) + + listener.emit('listen') + cb() + } + + listener.close = () => { + swarm.unhandle(multicodec) + listener.emit('close') + } + + listener.getAddrs = (callback) => { + let addrs = swarm._peerInfo.distinctMultiaddr().filter((addr) => { + return includes(addr.protoNames(), 'p2p-circuit') + }) + + callback(null, addrs) + } + + return listener +} diff --git a/src/multicodec.js b/src/multicodec.js new file mode 100644 index 0000000..3e585a4 --- /dev/null +++ b/src/multicodec.js @@ -0,0 +1 @@ +module.exports = '/ipfs/relay/circuit/1.0.0' \ No newline at end of file diff --git a/src/relay-session.js b/src/relay-session.js deleted file mode 100644 index 74cfd6b..0000000 --- a/src/relay-session.js +++ /dev/null @@ -1,92 +0,0 @@ -'use strict' - -const pull = require('pull-stream') -const handshake = require('pull-handshake') -const mss = require('multistream-select') -const config = require('./config') -const abortable = require('pull-abortable') -const utils = require('./utils') - -const log = config.log - -class RelaySession { - /** - * Constructs a relay RelaySession - * - * @param {Peer} src - the source peer - * @param {Peer} dst - the destination peer - */ - constructor (src, dst) { - this.src = src - this.dst = dst - this.srcStrAddr = null - this.dstStrAddr = null - this.circuitAddr = null - this.stream = abortable() - this.isActive = false - } - - /** - * is relay active - * - * @returns {Boolean} is stream active - */ - isActive () { - return this.isActive - } - - /** - * Circuit two connections - * - * @returns {void} - */ - circuit () { - let stream = handshake({timeout: 1000 * 60}, (err) => { - log.err(err) - this.stream.abort() - }) - - let shake = stream.handshake - - mss.util.writeEncoded( - shake, - utils.getDstAddrAsString(this.dst.peerInfo) - ) - - // create handshake stream - pull( - stream, - this.dst.conn, - stream - ) - - // circuit the src and dst streams - pull( - this.src.conn, - this.stream, - shake.rest(), - this.src.conn - ) - - this.isActive = true - } - - /** - * Closes the open connection to peer - * - * @param {Function} callback - * @returns {undefined} - */ - close (callback) { - // abort the circuit stream - if (this.stream) { - this.stream.abort() - } - setImmediate(() => { - this.stream = null - callback() - }) - } -} - -module.exports = RelaySession diff --git a/src/relay.js b/src/relay.js deleted file mode 100644 index eaa95b7..0000000 --- a/src/relay.js +++ /dev/null @@ -1,147 +0,0 @@ -'use strict' - -const pull = require('pull-stream') -const lp = require('pull-length-prefixed') -const multiaddr = require('multiaddr') -const Peer = require('./peer') -const handshake = require('pull-handshake') -const Connection = require('interface-connection').Connection -const RelaySession = require('./relay-session') -const utils = require('./utils') -const debug = require('debug') - -const multicodec = require('./config').multicodec - -const log = debug('libp2p:circuit:relay') -log.err = debug('libp2p:circuit:error:relay') - -class Relay { - constructor (libp2p) { - this.libp2p = libp2p - this.peers = new Map() - this.relaySessions = new Map() - - this._onConnection = this._onConnection.bind(this) - } - - start (cb) { - cb = cb || function () {} - this.libp2p.handle(multicodec, this._onConnection) - cb() - } - - stop (cb) { - cb = cb || function () {} - this.libp2p.unhandle(multicodec) - cb() - } - - _dialPeer (ma, callback) { - // let idB58Str - - // try { - // idB58Str = ma.getPeerId() // try to get the peerid from the multiaddr - // } catch (err) { - // log.err(err) - // } - - // if (idB58Str) { - // const peer = this.peers.get(idB58Str) - // if (peer && peer.isConnected) { - // return callback(null, peer) - // } - // } - - let addr = ma.toString() - if (addr.startsWith('/p2p-circuit')) { - addr = addr.substring(String('/p2p-circuit').length) - } - - this.libp2p.dialByMultiaddr(multiaddr(addr), multicodec, (err, conn) => { - if (err) { - log.err(err) - return callback(err) - } - - conn.getPeerInfo((err, peerInfo) => { - if (err) { - err(err) - return - } - - const idB58Str = peerInfo.id.toB58String() - // If already had a dial to me, just add the conn - if (!this.peers.has(idB58Str)) { - this.peers.set(idB58Str, new Peer(conn, peerInfo)) - } else { - this.peers.get(idB58Str).attachConnection(conn) - } - - callback(null, this.peers.get(idB58Str)) - }) - }) - } - - _onConnection (protocol, conn) { - setImmediate(() => conn.getPeerInfo((err, peerInfo) => { - if (err) { - log.err('Failed to identify incoming conn', err) - return pull(pull.empty(), conn) - } - - const idB58Str = peerInfo.id.toB58String() - let srcPeer = this.peers.get(idB58Str) - if (!srcPeer) { - log('new peer', idB58Str) - srcPeer = new Peer(conn, peerInfo) - this.peers.set(idB58Str, srcPeer) - } - this._processConnection(srcPeer, conn) - })) - } - - _processConnection (srcPeer, conn) { - let stream = handshake({timeout: 1000 * 60}) - let shake = stream.handshake - - lp.decodeFromReader(shake, (err, msg) => { - if (err) { - log.err(err) - return - } - - let addr = multiaddr(msg.toString()) - let newConn = new Connection(shake.rest(), conn) - srcPeer.attachConnection(newConn) - this._circuit(srcPeer, addr, (err) => { - if (err) { - log.err(err) - return null - } - }) - }) - - return pull(stream, conn, stream) - } - - _circuit (srcPeer, ma, callback) { - this._dialPeer(ma, (err, destPeer) => { - if (err) { - log.err(err) - return callback(err) - } - - let relaySession = new RelaySession(srcPeer, destPeer) - this.relaySessions.set( - utils.getCircuitStrAddr(srcPeer.peerInfo, destPeer.peerInfo), - relaySession - ) - - // create the circuit - relaySession.circuit() - return callback() - }) - } -} - -module.exports = Relay diff --git a/src/transport/index.js b/src/transport/index.js deleted file mode 100644 index 5b62db3..0000000 --- a/src/transport/index.js +++ /dev/null @@ -1,231 +0,0 @@ -'use strict' - -const config = require('../config') -const mss = require('multistream-select') -const pull = require('pull-stream') -const handshake = require('pull-handshake') -const Peer = require('../peer') -const Connection = require('interface-connection').Connection -const mafmt = require('mafmt') -const PeerInfo = require('peer-info') -const isFunction = require('lodash.isfunction') -const multiaddr = require('multiaddr') -const lp = require('pull-length-prefixed') - -const debug = require('debug') - -const log = debug('libp2p:circuit:dialer') -log.err = debug('libp2p:circuit:error:dialer') - -class Circuit { - /** - * Creates an instance of Dialer. - * @param {Swarm} swarm - the swarm - * - * @memberOf Dialer - */ - constructor (swarm) { - this.swarm = swarm - this.relayPeers = new Map() - this.peers = new Map() - - this.swarm.on('peer-mux-established', this._addRelayPeer.bind(this)) - this.swarm.on('peer-mux-closed', (peerInfo) => { - this.relayPeers.delete(peerInfo.id.toB58String()) - }) - } - - /** - * Dial a peer over a relay - * - * @param {multiaddr} ma - the multiaddr of the peer to dial - * @param {Object} options - dial options - * @param {Function} cb - a callback called once dialed - * @returns {Connection} - the connection - * - * @memberOf Dialer - */ - dial (ma, options, cb) { - if (isFunction(options)) { - cb = options - options = {} - } - - if (!cb) { - cb = () => { - } - } - - let idB58Str - - ma = multiaddr(ma) - try { - idB58Str = ma.getPeerId() // try to get the peerid from the multiaddr - } catch (err) { - log.err(err) - cb(err) - } - - if (this.peers.has(idB58Str)) { - return cb(null, - this.peers.get(idB58Str).conn) - } - - let dstConn = new Connection() - PeerInfo.create(idB58Str, (err, dstPeer) => { - if (err) { - log.err(err) - cb(err) - } - - dstConn.setPeerInfo(dstPeer) - dstPeer.multiaddr.add(ma) - - let relays = Array.from(this.relayPeers.values()) - let next = (relayPeer) => { - if (!relayPeer) { - return cb(`no relay peers were found!`) - } - - log(`Trying relay peer ${relayPeer.peerInfo.id.toB58String()}`) - this._dialRelay(relayPeer.peerInfo, (err, conn) => { - if (err) { - if (relays.length > 0) { - return next(relays.shift()) - } - return cb(err) - } - - this._negotiateRelay(conn, dstPeer, (err, conn) => { - if (err) { - log.err(`An error has occurred negotiating the relay connection`, err) - return cb(err) - } - - let stream = handshake({timeout: 1000 * 60}, cb) - let shake = stream.handshake - - lp.decodeFromReader(shake, (err, msg) => { - if (err) { - return cb(err) - } - - if (msg.toString() === 'ok') { - dstConn.setInnerConn(shake.rest()) - cb(null, dstConn) - } else { - return cb(new Error(`ko`)) - } - }) - - pull(stream, conn, stream) - this.peers.set(idB58Str, new Peer(conn, dstPeer)) - }) - }) - } - - next(relays.shift()) - }) - - return dstConn - } - - createListener (options, handler) { - throw new Error(`not implemented`) - } - - /** - * Negotiate the relay connection - * - * @param {Connection} conn - a connection to the relay - * @param {PeerInfo} peerInfo - the peerInfo of the peer to relay the connection for - * @param {Callback} callback - a callback with that return the negotiated relay connection - * @returns {void} - * - * @memberOf Dialer - */ - _negotiateRelay (conn, peerInfo, callback) { - let src = this.swarm._peerInfo.distinctMultiaddr() - let dst = peerInfo.distinctMultiaddr() - - if (!(src && src.length > 0) || !(dst && dst.length > 0)) { - log.err(`No valid multiaddress for peer!`) - callback(`No valid multiaddress for peer!`) - } - - let stream = handshake({timeout: 1000 * 60}, callback) - let shake = stream.handshake - - log(`negotiating relay for peer ${peerInfo.id}`) - mss.util.writeEncoded(shake, dst[0].toString()) - - pull(stream, conn, stream) - callback(null, new Connection(shake.rest(), conn)) - } - - /** - * Connect to a relay peer - * - * @param {PeerInfo} peerInfo - the PeerInfo of the relay - * @returns {void} - * - * @memberOf Dialer - */ - _addRelayPeer (peerInfo) { - if (!this.relayPeers.has(peerInfo.id.toB58String())) { - for (let ma of peerInfo.multiaddrs) { - if (mafmt.Circuit.matches(ma)) { - let peer = new Peer(null, peerInfo) - this.relayPeers.set(peerInfo.id.toB58String(), peer) - - // attempt to dia the relay so that we have a connection - this._dialRelay(peerInfo, (err, conn) => { - if (err) { - log.err(err) - return - } - peer.attachConnection(conn) - }) - break - } - } - } - } - - /** - * Dial a relay peer by its PeerInfo - * - * @param {PeerInfo} relayPeer - the PeerInfo of the relay peer - * @param {Function} callback - a callback with the connection to the relay peer - * @returns {void} - * - * @memberOf Dialer - */ - _dialRelay (relayPeer, callback) { - const idB58Str = relayPeer.id.toB58String() - log('dialing %s', idB58Str) - - if (this.peers.has(idB58Str)) { - return callback(null, this.peers.get(idB58Str)) - } - - this.swarm.dial(relayPeer, config.multicodec, (err, conn) => { - if (err) { - return callback(err) - } - - callback(null, conn) - }) - } - - filter (multiaddrs) { - if (!Array.isArray(multiaddrs)) { - multiaddrs = [multiaddrs] - } - return multiaddrs.filter((ma) => { - return mafmt.Circuit.matches(ma) - }) - } -} - -module.exports = Circuit diff --git a/src/transport/listener.js b/src/transport/listener.js deleted file mode 100644 index 48e1247..0000000 --- a/src/transport/listener.js +++ /dev/null @@ -1,112 +0,0 @@ -'use strict' - -const config = require('../config') -const pull = require('pull-stream') -const multiaddr = require('multiaddr') -const PeerInfo = require('peer-info') -const Peer = require('../peer') -const lp = require('pull-length-prefixed') -const handshake = require('pull-handshake') -const Connection = require('interface-connection').Connection -const EventEmitter = require('events').EventEmitter -const mss = require('multistream-select') - -const debug = require('debug') - -const multicodec = config.multicodec - -const log = debug('libp2p:circuit:listener') -log.err = debug('libp2p:circuit:error:listener') - -class Listener extends EventEmitter { - constructor (swarm) { - super() - this.swarm = swarm - this.peers = new Map() - - this._onConnection = this._onConnection.bind(this) - } - - listen (cb) { - cb = cb || (() => { - }) - this.swarm.handle(multicodec, this._onConnection) - this.emit('listening') - cb() - } - - close (options, cb) { - if (typeof options === 'function') { - cb = options - options = {} - } - cb = cb || (() => { - }) - options = options || {} - - this.swarm.unhandle(multicodec) - this.emit('close') - cb() - } - - _onConnection (protocol, conn) { - conn.getPeerInfo((err, peerInfo) => { - if (err) { - log.err('Failed to identify incoming conn', err) - this.emit('error', err) - return pull(pull.empty(), conn) - } - - const idB58Str = peerInfo.id.toB58String() - let relayPeer = this.peers.get(idB58Str) - if (!relayPeer) { - log('new relay peer', idB58Str) - relayPeer = peerInfo - this.peers.set(idB58Str, new Peer(conn, peerInfo)) - } - this._processConnection(relayPeer, conn, (err) => { - if (err) { - log.err(err) - this.emit('error', new Error(err)) - } - }) - }) - } - - _processConnection (relayPeer, conn, cb) { - let stream = handshake({timeout: 1000 * 60}) - let shake = stream.handshake - - lp.decodeFromReader(shake, (err, msg) => { - if (err) { - return cb(err) - } - - let addr = multiaddr(msg.toString()) - try { - PeerInfo.create(addr.getPeerId(), (err, peerInfo) => { - if (err) { - log.err(err) - this.handler(err) - } - - mss.util.writeEncoded(shake, 'ok') - peerInfo.multiaddr.add(addr) - const conn = new Connection(shake.rest(), peerInfo) - this.emit('connection', conn) - }) - } catch (err) { - cb(err) - } - }) - - pull(stream, conn, stream) - } -} - -module.exports = Listener -module.exports.listener = (swarm) => { - const listener = new Listener(swarm) - listener.listen() - return listener -} diff --git a/src/utils.js b/src/utils.js index 4dc6f31..d3169da 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,7 +1,8 @@ 'use strict' -const config = require('./config') +const debug = require('debug') -const log = config.log +const log = debug('libp2p:circuit:utils') +log.err = debug('libp2p:circuit:error:utils') function getDstAddrAsString (peerInfo) { return getStrAddress(peerInfo) diff --git a/test/index.spec.js b/test/index.spec.js index 5bab492..3d51b9d 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -3,20 +3,18 @@ const PeerInfo = require('peer-info') const series = require('async/series') +const parallel = require('async/parallel') const pull = require('pull-stream') const Libp2p = require('libp2p') const TCP = require('libp2p-tcp') const WS = require('libp2p-websockets') const spdy = require('libp2p-spdy') +const multiplex = require('libp2p-multiplex') const waterfall = require('async/waterfall') const expect = require('chai').expect -process.on('uncaughtException', function (err) { - console.log(err) -}) - class TestNode extends Libp2p { constructor (peerInfo, transports, options) { options = options || {} @@ -25,8 +23,8 @@ class TestNode extends Libp2p { transport: transports, connection: { muxer: [ - spdy - // multiplex + // spdy + multiplex ], crypto: [ // secio @@ -38,175 +36,26 @@ class TestNode extends Libp2p { } } -describe('circuit', function () { - describe('test common transports', function () { - let srcNode - let dstNode - let relayNode - - let srcPeer - let dstPeer - let relayPeer - - let portBase = 9000 // TODO: randomize or mock sockets - before((done) => { - series([ - (cb) => { - PeerInfo.create((err, info) => { - relayPeer = info - relayPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}`) - relayPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}/ws`) - relayNode = new TestNode(relayPeer, [new TCP(), new WS()]) - cb(err) - }) - }, - (cb) => { - PeerInfo.create((err, info) => { - srcPeer = info - srcPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}`) - srcNode = new TestNode(srcPeer, [new TCP()]) - srcNode.peerBook.put(relayPeer) - cb(err) - }) - }, - (cb) => { - PeerInfo.create((err, info) => { - dstPeer = info - dstPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}`) - dstNode = new TestNode(dstPeer, [new TCP()]) - srcNode.peerBook.put(relayPeer) - cb(err) - }) - } - ], (err) => done(err) - ) - }) - - beforeEach(function (done) { - series([ - (cb) => { - relayNode.start(cb) - }, - (cb) => { - srcNode.start(cb) - }, - (cb) => { - dstNode.start(cb) - } - ], (err) => done(err)) - }) - - afterEach(function circuitTests (done) { - series([ - (cb) => { - srcNode.stop(cb) - }, - (cb) => { - dstNode.stop(cb) - }, - (cb) => { - relayNode.stop(cb) - } - ], (err) => done(err)) - }) - - it('should dial from source to dest over common transports', function (done) { - this.timeout(500000) - - const handleTestProto = (proto, conn) => { - conn.getPeerInfo((err, peerInfo) => { - if (err) { - return done(err) - } - peerInfo.multiaddrs.forEach((ma) => { - console.log(ma.toString()) - }) - - pull(pull.values(['hello']), conn) - }) - } - - srcNode.handle('/ipfs/test/1.0.0', handleTestProto) - dstNode.handle('/ipfs/test/1.0.0', handleTestProto) - - waterfall([ - (cb) => srcNode.dialByPeerInfo(dstPeer, () => { - cb() - }), - (cb) => dstNode.dialByPeerInfo(srcPeer, () => { - cb() - }), - (cb) => { - waterfall([ - (cb) => { - srcNode.dialByPeerInfo(dstPeer, '/ipfs/test/1.0.0', (err, conn) => { - if (err) return cb(err) - - conn.getPeerInfo((err, peerInfo) => { - if (err) return cb(err) - - console.log(`from srcNode to relayNode:`) - peerInfo.multiaddrs.forEach((ma) => { - console.log(`src: ma of relay`, ma.toString()) - }) - - pull(conn, pull.collect((err, data) => { - if (err) return cb(err) - - data.forEach((dta) => { - console.log(dta.toString()) - }) - cb() - })) - }) - }) - }, - (cb) => { - dstNode.dialByPeerInfo(srcPeer, '/ipfs/test/1.0.0', (err, conn) => { - if (err) return cb(err) - - conn.getPeerInfo((err, peerInfo) => { - if (err) return cb(err) - - console.log(`from dstNode to relayNode:`) - peerInfo.multiaddrs.forEach((ma) => { - console.log(`dst: ma of relay`, ma.toString()) - }) - - pull(conn, pull.collect((err, data) => { - if (err) return cb(err) - - data.forEach((dta) => { - console.log(dta.toString()) - }) - cb() - })) - }) - }) - }], done) - } - ]) - }) - }) +describe('test relay', function () { + this.timeout(500000) - describe('test non common transports', function () { - let srcNode - let dstNode - let relayNode + let srcNode + let dstNode + let relayNode - let srcPeer - let dstPeer - let relayPeer + let srcPeer + let dstPeer + let relayPeer - let portBase = 9000 // TODO: randomize or mock sockets - before((done) => { - series([ + let portBase = 9000 // TODO: randomize or mock sockets + before((done) => { + series([ (cb) => { PeerInfo.create((err, info) => { relayPeer = info relayPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}`) relayPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}/ws`) - relayNode = new TestNode(relayPeer, [new TCP(), new WS()]) + relayNode = new TestNode(relayPeer, [new TCP(), new WS()], {relay: true}) cb(err) }) }, @@ -229,277 +78,89 @@ describe('circuit', function () { }) } ], (err) => done(err) - ) - }) - - beforeEach(function (done) { - series([ - (cb) => { - relayNode.start(cb) - }, - (cb) => { - srcNode.start(cb) - }, - (cb) => { - dstNode.start(cb) - } - ], (err) => done(err)) - }) - - afterEach(function circuitTests (done) { - series([ - (cb) => { - srcNode.stop(cb) - }, - (cb) => { - dstNode.stop(cb) - }, - (cb) => { - relayNode.stop(cb) - } - ], (err) => done(err)) - }) - - it('should dial to a common node over different transports', function (done) { - this.timeout(500000) - - const handleTestProto = (proto, conn) => { - conn.getPeerInfo((err, peerInfo) => { - if (err) { - return done(err) - } + ) + }) - console.log(`handling test proto:`) - peerInfo.multiaddrs.forEach((ma) => { - console.log(ma.toString()) - }) + beforeEach(function (done) { + series([ + (cb) => { + relayNode.start(cb) + }, + (cb) => { + srcNode.start(cb) + }, + (cb) => { + dstNode.start(cb) + }, + (cb) => srcNode.dialByPeerInfo(relayPeer, (err, conn) => { + cb() + }), + (cb) => dstNode.dialByPeerInfo(relayPeer, (err, conn) => { + cb() + }) + ], (err) => done(err)) + }) - pull(pull.values(['hello']), conn) - }) + afterEach(function circuitTests (done) { + series([ + (cb) => { + srcNode.stop(cb) + }, + (cb) => { + dstNode.stop(cb) + }, + (cb) => { + relayNode.stop(cb) } - - relayNode.handle('/ipfs/test/1.0.0', handleTestProto) - waterfall([ - (cb) => srcNode.dialByPeerInfo(relayPeer, () => { - cb() - }), - (cb) => dstNode.dialByPeerInfo(relayPeer, () => { - cb() - }), - (cb) => { - waterfall([ - // TODO: make sure the WebSockets node runs first, because TCP hangs the stream!!! possibly a bug.... - (cb) => { - dstNode.dialByPeerInfo(relayPeer, '/ipfs/test/1.0.0', (err, conn) => { - if (err) return cb(err) - - conn.getPeerInfo((err, peerInfo) => { - if (err) return cb(err) - - console.log(`from dstNode to relayNode:`) - peerInfo.multiaddrs.forEach((ma) => { - console.log(`dst: ma of relay`, ma.toString()) - }) - - pull(conn, pull.collect((err, data) => { - if (err) return cb(err) - - data.forEach((dta) => { - console.log(dta.toString()) - }) - cb() - })) - }) - }) - }, - (cb) => { - srcNode.dialByPeerInfo(relayPeer, '/ipfs/test/1.0.0', (err, conn) => { - if (err) return cb(err) - - conn.getPeerInfo((err, peerInfo) => { - if (err) return cb(err) - - console.log(`from srcNode to relayNode:`) - peerInfo.multiaddrs.forEach((ma) => { - console.log(`src: ma of relay`, ma.toString()) - }) - - pull(conn, pull.collect((err, data) => { - if (err) return cb(err) - - data.forEach((dta) => { - console.log(dta.toString()) - }) - cb() - })) - }) - }) - } - ], done) - } - ]) - }) + ], (err) => done(err)) }) - describe('test non common transports over relay', function () { - let srcNode - let dstNode - let relayNode - - let srcPeer - let dstPeer - let relayPeer - - let portBase = 9000 // TODO: randomize or mock sockets - before((done) => { - series([ - (cb) => { - PeerInfo.create((err, info) => { - relayPeer = info - relayPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}`) - relayPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}/ws`) - relayPeer.multiaddr.add(`/p2p-circuit`) - relayNode = new TestNode(relayPeer, [new TCP(), new WS()], {relay: true}) - cb(err) - }) - }, - (cb) => { - PeerInfo.create((err, info) => { - srcPeer = info - srcPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}`) - srcNode = new TestNode(srcPeer, [new TCP()]) - srcNode.peerBook.put(relayPeer) - cb(err) - }) - }, - (cb) => { - PeerInfo.create((err, info) => { - dstPeer = info - dstPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}/ws`) - dstNode = new TestNode(dstPeer, [new WS()]) - srcNode.peerBook.put(relayPeer) - cb(err) - }) - } - ], (err) => done(err) + describe('test basic circuit functionality', function () { + function reverse (protocol, conn) { + pull( + conn, + pull.map((data) => { + return data.toString().split('').reverse().join('') + }), + conn ) - }) - - beforeEach(function (done) { - series([ - (cb) => { - relayNode.start(cb) - }, - (cb) => { - srcNode.start(cb) - }, - (cb) => { - dstNode.start(cb) - } - ], (err) => done(err)) - }) - - afterEach(function circuitTests (done) { - series([ - (cb) => { - srcNode.stop(cb) - }, - (cb) => { - dstNode.stop(cb) - }, - (cb) => { - relayNode.stop(cb) - } - ], (err) => done(err)) - }) + } it('should dial to a node over a relay and write a value', function (done) { - this.timeout(500000) + srcNode.handle('/ipfs/reverse/1.0.0', reverse) - function reverse (protocol, conn) { + dstNode.dialByPeerInfo(srcPeer, '/ipfs/reverse/1.0.0', (err, conn) => { + if (err) return done(err) pull( + pull.values(['hello']), conn, - pull.map((data) => { - return data.toString().split('').reverse().join('') - }), - conn - ) - } - - srcNode.handle('/ipfs/reverse/1.0.0', reverse) - waterfall([ - (cb) => srcNode.dialByPeerInfo(relayPeer, () => { - cb() - }), - (cb) => dstNode.dialByPeerInfo(relayPeer, () => { - cb() - }), - (cb) => { - waterfall([ - // TODO: make sure the WebSockets dials first, because TCP hangs the stream!!! possibly a bug in TCP.... - (cb) => { - dstNode.dialByPeerInfo(srcPeer, '/ipfs/reverse/1.0.0', (err, conn) => { - if (err) return cb(err) - pull( - pull.values(['hello']), - conn, - pull.collect((err, data) => { - if (err) return cb(err) + pull.collect((err, data) => { + if (err) return cb(err) - expect(data[0].toString()).to.equal('olleh') - cb() - })) - }) - } - ], done) - } - ]) + expect(data[0].toString()).to.equal('olleh') + dstNode.hangUpByPeerInfo(srcPeer, done) + })) + }) }) - it('should dial to a node over a relay and write several value', function (done) { - this.timeout(500000) + it('should dial to a node over a relay and write several values', function (done) { + srcNode.handle('/ipfs/reverse/1.0.0', reverse) - function reverse (protocol, conn) { + dstNode.dialByPeerInfo(srcPeer, '/ipfs/reverse/1.0.0', (err, conn) => { + if (err) return cb(err) pull( + pull.values(['hello', 'hello1', 'hello2', 'hello3']), conn, - pull.map((data) => { - return data.toString().split('').reverse().join('') - }), - conn - ) - } - - srcNode.handle('/ipfs/reverse/1.0.0', reverse) - waterfall([ - (cb) => srcNode.dialByPeerInfo(relayPeer, () => { - cb() - }), - (cb) => dstNode.dialByPeerInfo(relayPeer, () => { - cb() - }), - (cb) => { - waterfall([ - // TODO: make sure the WebSockets dials first, because TCP hangs the stream!!! possibly a bug in TCP.... - (cb) => { - dstNode.dialByPeerInfo(srcPeer, '/ipfs/reverse/1.0.0', (err, conn) => { - if (err) return cb(err) - pull( - pull.values(['hello', 'hello1', 'hello2', 'hello3']), - conn, - pull.collect((err, data) => { - if (err) return cb(err) - - expect(data[0].toString()).to.equal('olleh') - expect(data[1].toString()).to.equal('1olleh') - expect(data[2].toString()).to.equal('2olleh') - expect(data[3].toString()).to.equal('3olleh') - cb() - })) - }) - } - ], done) - } - ]) + pull.collect((err, data) => { + if (err) return done(err) + + expect(data[0].toString()).to.equal('olleh') + expect(data[1].toString()).to.equal('1olleh') + expect(data[2].toString()).to.equal('2olleh') + expect(data[3].toString()).to.equal('3olleh') + dstNode.hangUpByPeerInfo(srcPeer, done) + })) + }) }) }) -}) +}) \ No newline at end of file From ae6dfa6d2dc80dd89771af86af991f02c016a383 Mon Sep 17 00:00:00 2001 From: dmitriy ryajov Date: Sat, 25 Mar 2017 15:15:38 -0700 Subject: [PATCH 11/15] test: adding additional tests --- src/{dialer.js => circuit-dialer.js} | 20 +-- src/index.js | 2 +- src/listener.js | 16 ++- test/index.spec.js | 186 +++++++++++++++------------ 4 files changed, 126 insertions(+), 98 deletions(-) rename src/{dialer.js => circuit-dialer.js} (95%) diff --git a/src/dialer.js b/src/circuit-dialer.js similarity index 95% rename from src/dialer.js rename to src/circuit-dialer.js index 6b6e667..3d8b543 100644 --- a/src/dialer.js +++ b/src/circuit-dialer.js @@ -18,12 +18,12 @@ const multicodec = require('./multicodec') const createListener = require('./listener') -class Dialer { +class CircuitDialer { /** * Creates an instance of Dialer. * @param {Swarm} swarm - the swarm * - * @memberOf Dialer + * @memberOf CircuitDialer */ constructor (swarm) { this.swarm = swarm @@ -43,7 +43,7 @@ class Dialer { * @param {Function} cb - a callback called once dialed * @returns {Connection} - the connection * - * @memberOf Dialer + * @memberOf CircuitDialer */ dial (ma, options, cb) { if (isFunction(options)) { @@ -95,7 +95,7 @@ class Dialer { * @param {Function} cb - callback to call with relayed connection or error * @returns {void} * - * @memberOf Dialer + * @memberOf CircuitDialer */ _initiateRelay (dstPeer, cb) { let relays = Array.from(this.relayPeers.values()) @@ -136,7 +136,7 @@ class Dialer { * @param {any} handler * @returns {Listener} * - * @memberOf Dialer + * @memberOf CircuitDialer */ createListener (handler) { return createListener(this.swarm, handler) @@ -150,7 +150,7 @@ class Dialer { * @param {Function} cb - a callback with that return the negotiated relay connection * @returns {void} * - * @memberOf Dialer + * @memberOf CircuitDialer */ _negotiateRelay (conn, peerInfo, cb) { let src = this.swarm._peerInfo.distinctMultiaddr() @@ -199,7 +199,7 @@ class Dialer { * @param {Function} callback - a callback with the connection to the relay peer * @returns {Function|void} * - * @memberOf Dialer + * @memberOf CircuitDialer */ _dialRelay (relayPeer, callback) { const idB58Str = relayPeer.id.toB58String() @@ -220,7 +220,7 @@ class Dialer { * @param {PeerInfo} peerInfo - the PeerInfo of the relay * @returns {void} * - * @memberOf Dialer + * @memberOf CircuitDialer */ _addRelayPeer (peerInfo) { // TODO: ask connected peers for all their connected peers @@ -251,7 +251,7 @@ class Dialer { * @param {any} multiaddrs * @returns {Array} * - * @memberOf Dialer + * @memberOf CircuitDialer */ filter (multiaddrs) { if (!Array.isArray(multiaddrs)) { @@ -263,4 +263,4 @@ class Dialer { } } -module.exports = Dialer +module.exports = CircuitDialer diff --git a/src/index.js b/src/index.js index e0f1017..b257ef8 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,5 @@ module.exports = { Circuit: require('./circuit'), - Dialer: require('./dialer'), + CircuitDialer: require('./circuit-dialer'), multicodec: require('./multicodec') } \ No newline at end of file diff --git a/src/listener.js b/src/listener.js index de832d2..cdc44db 100644 --- a/src/listener.js +++ b/src/listener.js @@ -9,6 +9,7 @@ const lp = require('pull-length-prefixed') const multiaddr = require('multiaddr') const handshake = require('pull-handshake') const Connection = require('interface-connection').Connection +const abortable = require('pull-abortable') const debug = require('debug') @@ -53,8 +54,8 @@ module.exports = (swarm, handler) => { }) } else { // we need this to signal the circuit that the connection is ready - // otherwise, it seems be get circuited prematurely, which causes the - // dialer to fail on a non ready connection + // otherwise, the circuit will happen prematurely, which causes the + // dialer to fail since the connection is not ready shake.write('\n') let newConn = new Connection(shake.rest(), conn) listener.emit('connection', newConn) @@ -62,7 +63,10 @@ module.exports = (swarm, handler) => { } }) - pull(stream, conn, stream) + pull( + stream, + conn, + stream) }) }) @@ -70,9 +74,13 @@ module.exports = (swarm, handler) => { cb() } - listener.close = () => { + listener.close = (cb) => { + // TODO: should we close/abort connections here? + // spdy-transport throw a `Error: socket hang up` + // on swarm stop right now, could be an existing issue? swarm.unhandle(multicodec) listener.emit('close') + cb() } listener.getAddrs = (callback) => { diff --git a/test/index.spec.js b/test/index.spec.js index 3d51b9d..12ee1ab 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -12,22 +12,20 @@ const WS = require('libp2p-websockets') const spdy = require('libp2p-spdy') const multiplex = require('libp2p-multiplex') const waterfall = require('async/waterfall') +const secio = require('libp2p-secio') const expect = require('chai').expect class TestNode extends Libp2p { - constructor (peerInfo, transports, options) { + constructor (peerInfo, transports, muxer, options) { options = options || {} const modules = { transport: transports, connection: { - muxer: [ - // spdy - multiplex - ], + muxer: [muxer], crypto: [ - // secio + secio ] }, discovery: [] @@ -48,41 +46,42 @@ describe('test relay', function () { let relayPeer let portBase = 9000 // TODO: randomize or mock sockets - before((done) => { + + function setUpNodes (muxer, cb) { series([ - (cb) => { - PeerInfo.create((err, info) => { - relayPeer = info - relayPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}`) - relayPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}/ws`) - relayNode = new TestNode(relayPeer, [new TCP(), new WS()], {relay: true}) - cb(err) - }) - }, - (cb) => { - PeerInfo.create((err, info) => { - srcPeer = info - srcPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}`) - srcNode = new TestNode(srcPeer, [new TCP()]) - srcNode.peerBook.put(relayPeer) - cb(err) - }) - }, - (cb) => { - PeerInfo.create((err, info) => { - dstPeer = info - dstPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}/ws`) - dstNode = new TestNode(dstPeer, [new WS()]) - srcNode.peerBook.put(relayPeer) - cb(err) - }) - } - ], (err) => done(err) - ) - }) + (cb) => { + PeerInfo.create((err, info) => { + relayPeer = info + relayPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}`) + relayPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}/ws`) + relayNode = new TestNode(relayPeer, [new TCP(), new WS()], muxer, {relay: true}) + cb(err) + }) + }, + (cb) => { + PeerInfo.create((err, info) => { + srcPeer = info + srcPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}`) + srcNode = new TestNode(srcPeer, [new TCP()], muxer) + srcNode.peerBook.put(relayPeer) + cb(err) + }) + }, + (cb) => { + PeerInfo.create((err, info) => { + dstPeer = info + dstPeer.multiaddr.add(`/ip4/0.0.0.0/tcp/${portBase++}/ws`) + dstNode = new TestNode(dstPeer, [new WS()], muxer) + srcNode.peerBook.put(relayPeer) + cb(err) + }) + } + ], cb) + } - beforeEach(function (done) { + function startNodes (muxer, done) { series([ + (cb) => setUpNodes(muxer, cb), (cb) => { relayNode.start(cb) }, @@ -98,10 +97,11 @@ describe('test relay', function () { (cb) => dstNode.dialByPeerInfo(relayPeer, (err, conn) => { cb() }) - ], (err) => done(err)) - }) + ], done) + + } - afterEach(function circuitTests (done) { + function stopNodes (done) { series([ (cb) => { srcNode.stop(cb) @@ -112,55 +112,75 @@ describe('test relay', function () { (cb) => { relayNode.stop(cb) } - ], (err) => done(err)) - }) + ], (err) => done()) // TODO: pass err to done once we figure out why spdy is throwing on stop + } + + function reverse (protocol, conn) { + pull( + conn, + pull.map((data) => { + return data.toString().split('').reverse().join('') + }), + conn + ) + } + + function dialAndRevers (vals, done) { + srcNode.handle('/ipfs/reverse/1.0.0', reverse) + + dstNode.dialByPeerInfo(srcPeer, '/ipfs/reverse/1.0.0', (err, conn) => { + if (err) return done(err) - describe('test basic circuit functionality', function () { - function reverse (protocol, conn) { pull( + pull.values(['hello']), conn, - pull.map((data) => { - return data.toString().split('').reverse().join('') - }), - conn - ) - } + pull.collect((err, data) => { + if (err) return cb(err) + + data.forEach((val, i) => { + expect(val.toString()).to.equal(vals[i].split('').reverse().join('')) + }) + + dstNode.hangUpByPeerInfo(srcPeer, done) + })) + }) + } + + describe(`circuit over spdy muxer`, function () { + beforeEach(function (done) { + startNodes(spdy, done) + }) + + afterEach(function circuitTests (done) { + stopNodes(done) + }) it('should dial to a node over a relay and write a value', function (done) { - srcNode.handle('/ipfs/reverse/1.0.0', reverse) - - dstNode.dialByPeerInfo(srcPeer, '/ipfs/reverse/1.0.0', (err, conn) => { - if (err) return done(err) - pull( - pull.values(['hello']), - conn, - pull.collect((err, data) => { - if (err) return cb(err) - - expect(data[0].toString()).to.equal('olleh') - dstNode.hangUpByPeerInfo(srcPeer, done) - })) - }) + dialAndRevers(['hello'], done) }) it('should dial to a node over a relay and write several values', function (done) { - srcNode.handle('/ipfs/reverse/1.0.0', reverse) - - dstNode.dialByPeerInfo(srcPeer, '/ipfs/reverse/1.0.0', (err, conn) => { - if (err) return cb(err) - pull( - pull.values(['hello', 'hello1', 'hello2', 'hello3']), - conn, - pull.collect((err, data) => { - if (err) return done(err) - - expect(data[0].toString()).to.equal('olleh') - expect(data[1].toString()).to.equal('1olleh') - expect(data[2].toString()).to.equal('2olleh') - expect(data[3].toString()).to.equal('3olleh') - dstNode.hangUpByPeerInfo(srcPeer, done) - })) - }) + dialAndRevers(['hello', 'hello1', 'hello2', 'hello3'], done) + }) + + }) + + describe(`circuit over multiplex muxer`, function () { + beforeEach(function (done) { + startNodes(multiplex, done) + }) + + afterEach(function circuitTests (done) { + stopNodes(done) + }) + + it('should dial to a node over a relay and write a value', function (done) { + dialAndRevers(['hello'], done) + }) + + it('should dial to a node over a relay and write several values', function (done) { + dialAndRevers(['hello', 'hello1', 'hello2', 'hello3'], done) }) + }) -}) \ No newline at end of file +}) From bea48ed0cb1fa918c6df97bb35fcc417ee3cdab3 Mon Sep 17 00:00:00 2001 From: dmitriy ryajov Date: Sat, 25 Mar 2017 01:47:09 -0700 Subject: [PATCH 12/15] feat: rework as a transport --- src/dialer.js | 266 +++++++++++++++++++++++++++++++++++++++++++++ test/index.spec.js | 2 +- 2 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 src/dialer.js diff --git a/src/dialer.js b/src/dialer.js new file mode 100644 index 0000000..6b6e667 --- /dev/null +++ b/src/dialer.js @@ -0,0 +1,266 @@ +'use strict' + +const pull = require('pull-stream') +const handshake = require('pull-handshake') +const Peer = require('./peer') +const Connection = require('interface-connection').Connection +const mafmt = require('mafmt') +const PeerInfo = require('peer-info') +const isFunction = require('lodash.isfunction') +const multiaddr = require('multiaddr') +const lp = require('pull-length-prefixed') +const debug = require('debug') + +const log = debug('libp2p:circuit:dialer') +log.err = debug('libp2p:circuit:error:dialer') + +const multicodec = require('./multicodec') + +const createListener = require('./listener') + +class Dialer { + /** + * Creates an instance of Dialer. + * @param {Swarm} swarm - the swarm + * + * @memberOf Dialer + */ + constructor (swarm) { + this.swarm = swarm + this.relayPeers = new Map() + + this.swarm.on('peer-mux-established', this._addRelayPeer.bind(this)) + this.swarm.on('peer-mux-closed', (peerInfo) => { + this.relayPeers.delete(peerInfo.id.toB58String()) + }) + } + + /** + * Dial a peer over a relay + * + * @param {multiaddr} ma - the multiaddr of the peer to dial + * @param {Object} options - dial options + * @param {Function} cb - a callback called once dialed + * @returns {Connection} - the connection + * + * @memberOf Dialer + */ + dial (ma, options, cb) { + if (isFunction(options)) { + cb = options + options = {} + } + + if (!cb) { + cb = () => { + } + } + + let idB58Str + ma = multiaddr(ma) + idB58Str = ma.getPeerId() // try to get the peerId from the multiaddr + if (!idB58Str) { + let err = 'No valid peer id in multiaddr' + log.err(err) + cb(err) + } + + let dstConn = new Connection() + PeerInfo.create(idB58Str, (err, dstPeer) => { + if (err) { + log.err(err) + cb(err) + } + + dstConn.setPeerInfo(dstPeer) + dstPeer.multiaddr.add(ma) + this._initiateRelay(dstPeer, (err, conn) => { + if (err) { + log.err(err) + return dstConn.setInnerConn(pull.empty()) + } + + dstConn.setInnerConn(conn) + cb(null, dstConn) + }) + }) + + return dstConn + } + + /** + * Initate the relay connection + * + * @param {PeerInfo} dstPeer - the destination peer + * @param {Function} cb - callback to call with relayed connection or error + * @returns {void} + * + * @memberOf Dialer + */ + _initiateRelay (dstPeer, cb) { + let relays = Array.from(this.relayPeers.values()) + let next = (relayPeer) => { + if (!relayPeer) { + const err = `no relay peers were found!` + log.err(err) + return cb(err) + } + + log(`Trying relay peer ${relayPeer.peerInfo.id.toB58String()}`) + this._dialRelay(relayPeer.peerInfo, (err, conn) => { + if (err) { + if (relays.length > 0) { + return next(relays.shift()) + } + return cb(err) + } + + this._negotiateRelay(conn, dstPeer, (err, conn) => { + if (err) { + log.err(`An error has occurred negotiating the relay connection`, err) + return cb(err) + } + + return cb(null, conn) + }) + }) + } + + next(relays.shift()) + } + + /** + * Create listener + * + * @param {any} options + * @param {any} handler + * @returns {Listener} + * + * @memberOf Dialer + */ + createListener (handler) { + return createListener(this.swarm, handler) + } + + /** + * Negotiate the relay connection + * + * @param {Connection} conn - a connection to the relay + * @param {PeerInfo} peerInfo - the peerInfo of the peer to relay the connection for + * @param {Function} cb - a callback with that return the negotiated relay connection + * @returns {void} + * + * @memberOf Dialer + */ + _negotiateRelay (conn, peerInfo, cb) { + let src = this.swarm._peerInfo.distinctMultiaddr() + let dst = peerInfo.distinctMultiaddr() + + if (!(src && src.length > 0) || !(dst && dst.length > 0)) { + let err = `No valid multiaddress for peer!` + log.err(err) + cb(err) + } + + let stream = handshake({timeout: 1000 * 60}, cb) + let shake = stream.handshake + + log(`negotiating relay for peer ${peerInfo.id}`) + + const values = [new Buffer(dst[0].toString())] + + pull( + pull.values(values), + lp.encode(), + pull.collect((err, encoded) => { + if (err) { + return cb(err) + } + + shake.write(encoded[0]) + shake.read(1, (err, data) => { + if (err) { + log.err(err) + return cb(err) + } + + cb(null, shake.rest()) + }) + }) + ) + + pull(stream, conn, stream) + } + + /** + * Dial a relay peer by its PeerInfo + * + * @param {PeerInfo} relayPeer - the PeerInfo of the relay peer + * @param {Function} callback - a callback with the connection to the relay peer + * @returns {Function|void} + * + * @memberOf Dialer + */ + _dialRelay (relayPeer, callback) { + const idB58Str = relayPeer.id.toB58String() + log('dialing relay %s', idB58Str) + + this.swarm.dial(relayPeer, multicodec, (err, conn) => { + if (err) { + return callback(err) + } + + callback(null, conn) + }) + } + + /** + * Connect to a relay peer + * + * @param {PeerInfo} peerInfo - the PeerInfo of the relay + * @returns {void} + * + * @memberOf Dialer + */ + _addRelayPeer (peerInfo) { + // TODO: ask connected peers for all their connected peers + // as well and try to establish a relay to them as well + + // TODO: ask peers if they can proactively dial on your behalf to other peers (active/passive) + // should it be a multistream header? + + if (!this.relayPeers.has(peerInfo.id.toB58String())) { + let peer = new Peer(null, peerInfo) + this.relayPeers.set(peerInfo.id.toB58String(), peer) + + // attempt to dia the relay so that we have a connection + this._dialRelay(peerInfo, (err, conn) => { + if (err) { + log.err(err) + return + } + peer.attachConnection(conn) + }) + } + } + + /** + * Filter check for all multiaddresses + * that this transport can dial on + * + * @param {any} multiaddrs + * @returns {Array} + * + * @memberOf Dialer + */ + filter (multiaddrs) { + if (!Array.isArray(multiaddrs)) { + multiaddrs = [multiaddrs] + } + return multiaddrs.filter((ma) => { + return mafmt.Circuit.matches(ma) + }) + } +} + +module.exports = Dialer diff --git a/test/index.spec.js b/test/index.spec.js index 12ee1ab..ca4c73b 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -183,4 +183,4 @@ describe('test relay', function () { }) }) -}) +}) \ No newline at end of file From fb8f4f789064e73f8a3bc4b8da49e1ae0e56553f Mon Sep 17 00:00:00 2001 From: dmitriy ryajov Date: Sat, 25 Mar 2017 15:15:38 -0700 Subject: [PATCH 13/15] test: adding more tests --- src/dialer.js | 266 --------------------------------------------- test/index.spec.js | 2 +- 2 files changed, 1 insertion(+), 267 deletions(-) delete mode 100644 src/dialer.js diff --git a/src/dialer.js b/src/dialer.js deleted file mode 100644 index 6b6e667..0000000 --- a/src/dialer.js +++ /dev/null @@ -1,266 +0,0 @@ -'use strict' - -const pull = require('pull-stream') -const handshake = require('pull-handshake') -const Peer = require('./peer') -const Connection = require('interface-connection').Connection -const mafmt = require('mafmt') -const PeerInfo = require('peer-info') -const isFunction = require('lodash.isfunction') -const multiaddr = require('multiaddr') -const lp = require('pull-length-prefixed') -const debug = require('debug') - -const log = debug('libp2p:circuit:dialer') -log.err = debug('libp2p:circuit:error:dialer') - -const multicodec = require('./multicodec') - -const createListener = require('./listener') - -class Dialer { - /** - * Creates an instance of Dialer. - * @param {Swarm} swarm - the swarm - * - * @memberOf Dialer - */ - constructor (swarm) { - this.swarm = swarm - this.relayPeers = new Map() - - this.swarm.on('peer-mux-established', this._addRelayPeer.bind(this)) - this.swarm.on('peer-mux-closed', (peerInfo) => { - this.relayPeers.delete(peerInfo.id.toB58String()) - }) - } - - /** - * Dial a peer over a relay - * - * @param {multiaddr} ma - the multiaddr of the peer to dial - * @param {Object} options - dial options - * @param {Function} cb - a callback called once dialed - * @returns {Connection} - the connection - * - * @memberOf Dialer - */ - dial (ma, options, cb) { - if (isFunction(options)) { - cb = options - options = {} - } - - if (!cb) { - cb = () => { - } - } - - let idB58Str - ma = multiaddr(ma) - idB58Str = ma.getPeerId() // try to get the peerId from the multiaddr - if (!idB58Str) { - let err = 'No valid peer id in multiaddr' - log.err(err) - cb(err) - } - - let dstConn = new Connection() - PeerInfo.create(idB58Str, (err, dstPeer) => { - if (err) { - log.err(err) - cb(err) - } - - dstConn.setPeerInfo(dstPeer) - dstPeer.multiaddr.add(ma) - this._initiateRelay(dstPeer, (err, conn) => { - if (err) { - log.err(err) - return dstConn.setInnerConn(pull.empty()) - } - - dstConn.setInnerConn(conn) - cb(null, dstConn) - }) - }) - - return dstConn - } - - /** - * Initate the relay connection - * - * @param {PeerInfo} dstPeer - the destination peer - * @param {Function} cb - callback to call with relayed connection or error - * @returns {void} - * - * @memberOf Dialer - */ - _initiateRelay (dstPeer, cb) { - let relays = Array.from(this.relayPeers.values()) - let next = (relayPeer) => { - if (!relayPeer) { - const err = `no relay peers were found!` - log.err(err) - return cb(err) - } - - log(`Trying relay peer ${relayPeer.peerInfo.id.toB58String()}`) - this._dialRelay(relayPeer.peerInfo, (err, conn) => { - if (err) { - if (relays.length > 0) { - return next(relays.shift()) - } - return cb(err) - } - - this._negotiateRelay(conn, dstPeer, (err, conn) => { - if (err) { - log.err(`An error has occurred negotiating the relay connection`, err) - return cb(err) - } - - return cb(null, conn) - }) - }) - } - - next(relays.shift()) - } - - /** - * Create listener - * - * @param {any} options - * @param {any} handler - * @returns {Listener} - * - * @memberOf Dialer - */ - createListener (handler) { - return createListener(this.swarm, handler) - } - - /** - * Negotiate the relay connection - * - * @param {Connection} conn - a connection to the relay - * @param {PeerInfo} peerInfo - the peerInfo of the peer to relay the connection for - * @param {Function} cb - a callback with that return the negotiated relay connection - * @returns {void} - * - * @memberOf Dialer - */ - _negotiateRelay (conn, peerInfo, cb) { - let src = this.swarm._peerInfo.distinctMultiaddr() - let dst = peerInfo.distinctMultiaddr() - - if (!(src && src.length > 0) || !(dst && dst.length > 0)) { - let err = `No valid multiaddress for peer!` - log.err(err) - cb(err) - } - - let stream = handshake({timeout: 1000 * 60}, cb) - let shake = stream.handshake - - log(`negotiating relay for peer ${peerInfo.id}`) - - const values = [new Buffer(dst[0].toString())] - - pull( - pull.values(values), - lp.encode(), - pull.collect((err, encoded) => { - if (err) { - return cb(err) - } - - shake.write(encoded[0]) - shake.read(1, (err, data) => { - if (err) { - log.err(err) - return cb(err) - } - - cb(null, shake.rest()) - }) - }) - ) - - pull(stream, conn, stream) - } - - /** - * Dial a relay peer by its PeerInfo - * - * @param {PeerInfo} relayPeer - the PeerInfo of the relay peer - * @param {Function} callback - a callback with the connection to the relay peer - * @returns {Function|void} - * - * @memberOf Dialer - */ - _dialRelay (relayPeer, callback) { - const idB58Str = relayPeer.id.toB58String() - log('dialing relay %s', idB58Str) - - this.swarm.dial(relayPeer, multicodec, (err, conn) => { - if (err) { - return callback(err) - } - - callback(null, conn) - }) - } - - /** - * Connect to a relay peer - * - * @param {PeerInfo} peerInfo - the PeerInfo of the relay - * @returns {void} - * - * @memberOf Dialer - */ - _addRelayPeer (peerInfo) { - // TODO: ask connected peers for all their connected peers - // as well and try to establish a relay to them as well - - // TODO: ask peers if they can proactively dial on your behalf to other peers (active/passive) - // should it be a multistream header? - - if (!this.relayPeers.has(peerInfo.id.toB58String())) { - let peer = new Peer(null, peerInfo) - this.relayPeers.set(peerInfo.id.toB58String(), peer) - - // attempt to dia the relay so that we have a connection - this._dialRelay(peerInfo, (err, conn) => { - if (err) { - log.err(err) - return - } - peer.attachConnection(conn) - }) - } - } - - /** - * Filter check for all multiaddresses - * that this transport can dial on - * - * @param {any} multiaddrs - * @returns {Array} - * - * @memberOf Dialer - */ - filter (multiaddrs) { - if (!Array.isArray(multiaddrs)) { - multiaddrs = [multiaddrs] - } - return multiaddrs.filter((ma) => { - return mafmt.Circuit.matches(ma) - }) - } -} - -module.exports = Dialer diff --git a/test/index.spec.js b/test/index.spec.js index ca4c73b..12ee1ab 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -183,4 +183,4 @@ describe('test relay', function () { }) }) -}) \ No newline at end of file +}) From 64a0a14fc4317bea3fc9da8d89bf5b0d99022ab5 Mon Sep 17 00:00:00 2001 From: dmitriy ryajov Date: Sat, 25 Mar 2017 15:53:02 -0700 Subject: [PATCH 14/15] style: fixing lint issues --- package.json | 5 ++++ src/circuit-dialer.js | 10 ++++---- src/{circuit.js => circuit-relay.js} | 34 +++++++++++++++++++--------- src/index.js | 6 +++-- src/listener.js | 5 ++-- src/multicodec.js | 4 +++- test/index.spec.js | 14 ++++-------- 7 files changed, 45 insertions(+), 33 deletions(-) rename src/{circuit.js => circuit-relay.js} (82%) diff --git a/package.json b/package.json index 554dd26..170d0af 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,11 @@ "url": "https://github.com/libp2p/js-libp2p-circuit/issues" }, "homepage": "https://github.com/libp2p/js-libp2p-circuit#readme", + "eslintConfig": { + "extends": [ + "./node_modules/aegir/config/eslintrc.yml" + ] + }, "devDependencies": { "aegir": "^10.0.0", "chai": "^3.5.0", diff --git a/src/circuit-dialer.js b/src/circuit-dialer.js index 3d8b543..e02729a 100644 --- a/src/circuit-dialer.js +++ b/src/circuit-dialer.js @@ -130,15 +130,13 @@ class CircuitDialer { } /** - * Create listener + * Create a listener * + * @param {Function} handler * @param {any} options - * @param {any} handler - * @returns {Listener} - * - * @memberOf CircuitDialer + * @returns {listener} */ - createListener (handler) { + createListener (handler, options) { return createListener(this.swarm, handler) } diff --git a/src/circuit.js b/src/circuit-relay.js similarity index 82% rename from src/circuit.js rename to src/circuit-relay.js index 74ad2f5..457ab1e 100644 --- a/src/circuit.js +++ b/src/circuit-relay.js @@ -2,19 +2,16 @@ const pull = require('pull-stream') const lp = require('pull-length-prefixed') -const multiaddr = require('multiaddr') const Peer = require('./peer') const handshake = require('pull-handshake') -const utils = require('./utils') const debug = require('debug') -const includes = require('lodash/includes') const PeerInfo = require('peer-info') const PeerId = require('peer-id') const multicodec = require('./multicodec') -const log = debug('libp2p:circuit:circuit') -log.err = debug('libp2p:circuit:error:circuit') +const log = debug('libp2p:circuit:relay') +log.err = debug('libp2p:circuit:error:relay') class Circuit { @@ -36,11 +33,13 @@ class Circuit { } /** + * The handler called to process a connection * - * @param conn - * @param peerInfo - * @param dstAddr - * @param cb + * @param {Connection} conn + * @param {Multiaddr} dstAddr + * @param {Function} cb + * + * @return {void} */ handler (conn, dstAddr, cb) { this._circuit(conn, dstAddr, cb) @@ -74,6 +73,15 @@ class Circuit { }) } + /** + * Circuit two peers + * + * @param {Connection} srcConn + * @param {Multiaddr} dstMa + * @param {Function} cb + * @return {void} + * @private + */ _circuit (srcConn, dstMa, cb) { this._dialPeer(dstMa, (err, dstConn) => { if (err) { @@ -85,6 +93,11 @@ class Circuit { let shake = stream.handshake dstConn.getPeerInfo((err, peerInfo) => { + if (err) { + log.err(err) + return cb(err) + } + pull( pull.values([new Buffer(`/ipfs/${peerInfo.id.toB58String()}`)]), lp.encode(), @@ -111,9 +124,8 @@ class Circuit { dstConn, stream ) - }) } } -module.exports = Circuit \ No newline at end of file +module.exports = Circuit diff --git a/src/index.js b/src/index.js index b257ef8..ed941bc 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,7 @@ +'use strict' + module.exports = { - Circuit: require('./circuit'), + Circuit: require('./circuit-relay'), CircuitDialer: require('./circuit-dialer'), multicodec: require('./multicodec') -} \ No newline at end of file +} diff --git a/src/listener.js b/src/listener.js index cdc44db..7f98d43 100644 --- a/src/listener.js +++ b/src/listener.js @@ -2,14 +2,13 @@ const includes = require('lodash/includes') const pull = require('pull-stream') -const Circuit = require('./circuit') +const Circuit = require('./circuit-relay') const multicodec = require('./multicodec') const EE = require('events').EventEmitter const lp = require('pull-length-prefixed') const multiaddr = require('multiaddr') const handshake = require('pull-handshake') const Connection = require('interface-connection').Connection -const abortable = require('pull-abortable') const debug = require('debug') @@ -76,7 +75,7 @@ module.exports = (swarm, handler) => { listener.close = (cb) => { // TODO: should we close/abort connections here? - // spdy-transport throw a `Error: socket hang up` + // spdy-transport throws a `Error: socket hang up` // on swarm stop right now, could be an existing issue? swarm.unhandle(multicodec) listener.emit('close') diff --git a/src/multicodec.js b/src/multicodec.js index 3e585a4..1d032a3 100644 --- a/src/multicodec.js +++ b/src/multicodec.js @@ -1 +1,3 @@ -module.exports = '/ipfs/relay/circuit/1.0.0' \ No newline at end of file +'use strict' + +module.exports = '/ipfs/relay/circuit/1.0.0' diff --git a/test/index.spec.js b/test/index.spec.js index 12ee1ab..4b82efe 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -3,7 +3,6 @@ const PeerInfo = require('peer-info') const series = require('async/series') -const parallel = require('async/parallel') const pull = require('pull-stream') const Libp2p = require('libp2p') @@ -11,7 +10,6 @@ const TCP = require('libp2p-tcp') const WS = require('libp2p-websockets') const spdy = require('libp2p-spdy') const multiplex = require('libp2p-multiplex') -const waterfall = require('async/waterfall') const secio = require('libp2p-secio') const expect = require('chai').expect @@ -91,12 +89,8 @@ describe('test relay', function () { (cb) => { dstNode.start(cb) }, - (cb) => srcNode.dialByPeerInfo(relayPeer, (err, conn) => { - cb() - }), - (cb) => dstNode.dialByPeerInfo(relayPeer, (err, conn) => { - cb() - }) + (cb) => srcNode.dialByPeerInfo(relayPeer, cb), + (cb) => dstNode.dialByPeerInfo(relayPeer, cb) ], done) } @@ -112,7 +106,7 @@ describe('test relay', function () { (cb) => { relayNode.stop(cb) } - ], (err) => done()) // TODO: pass err to done once we figure out why spdy is throwing on stop + ], () => done()) // TODO: pass err to done once we figure out why spdy is throwing on stop } function reverse (protocol, conn) { @@ -135,7 +129,7 @@ describe('test relay', function () { pull.values(['hello']), conn, pull.collect((err, data) => { - if (err) return cb(err) + if (err) return done(err) data.forEach((val, i) => { expect(val.toString()).to.equal(vals[i].split('').reverse().join('')) From 837cc20b27c12b8de2b7909615224c9cd34c949a Mon Sep 17 00:00:00 2001 From: dmitriy ryajov Date: Sat, 25 Mar 2017 16:03:27 -0700 Subject: [PATCH 15/15] style: removed extra spaces --- test/index.spec.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/index.spec.js b/test/index.spec.js index 4b82efe..4af2604 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -92,7 +92,6 @@ describe('test relay', function () { (cb) => srcNode.dialByPeerInfo(relayPeer, cb), (cb) => dstNode.dialByPeerInfo(relayPeer, cb) ], done) - } function stopNodes (done) { @@ -156,7 +155,6 @@ describe('test relay', function () { it('should dial to a node over a relay and write several values', function (done) { dialAndRevers(['hello', 'hello1', 'hello2', 'hello3'], done) }) - }) describe(`circuit over multiplex muxer`, function () { @@ -175,6 +173,5 @@ describe('test relay', function () { it('should dial to a node over a relay and write several values', function (done) { dialAndRevers(['hello', 'hello1', 'hello2', 'hello3'], done) }) - }) })