diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1dd9938 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# 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 + +.history +.vscode 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/.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/.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/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..17244b8 100644 --- a/README.md +++ b/README.md @@ -1 +1,38 @@ -# js-libp2p-circuit +# + +[![](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/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/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/package.json b/package.json new file mode 100644 index 0000000..170d0af --- /dev/null +++ b/package.json @@ -0,0 +1,69 @@ +{ + "name": "libp2p-circuit", + "version": "0.0.1-alpha.1", + "description": "JavaScript implementation of circuit/switch relaying", + "main": "src/index.js", + "scripts": { + "lint": "aegir-lint", + "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" + }, + "pre-commit": [ + "lint", + "test" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p-circuit.git" + }, + "keywords": [ + "IPFS" + ], + "author": "Dmitriy Ryajov ", + "license": "MIT", + "bugs": { + "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", + "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" + }, + "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-abortable": "^4.1.0", + "pull-handshake": "^1.1.4", + "pull-stream": "^3.5.0", + "setimmediate": "^1.0.5" + } +} diff --git a/src/circuit-dialer.js b/src/circuit-dialer.js new file mode 100644 index 0000000..e02729a --- /dev/null +++ b/src/circuit-dialer.js @@ -0,0 +1,264 @@ +'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 CircuitDialer { + /** + * Creates an instance of Dialer. + * @param {Swarm} swarm - the swarm + * + * @memberOf CircuitDialer + */ + 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 CircuitDialer + */ + 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 CircuitDialer + */ + _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 a listener + * + * @param {Function} handler + * @param {any} options + * @returns {listener} + */ + createListener (handler, options) { + 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 CircuitDialer + */ + _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 CircuitDialer + */ + _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 CircuitDialer + */ + _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 CircuitDialer + */ + filter (multiaddrs) { + if (!Array.isArray(multiaddrs)) { + multiaddrs = [multiaddrs] + } + return multiaddrs.filter((ma) => { + return mafmt.Circuit.matches(ma) + }) + } +} + +module.exports = CircuitDialer diff --git a/src/circuit-relay.js b/src/circuit-relay.js new file mode 100644 index 0000000..457ab1e --- /dev/null +++ b/src/circuit-relay.js @@ -0,0 +1,131 @@ +'use strict' + +const pull = require('pull-stream') +const lp = require('pull-length-prefixed') +const Peer = require('./peer') +const handshake = require('pull-handshake') +const debug = require('debug') +const PeerInfo = require('peer-info') +const PeerId = require('peer-id') + +const multicodec = require('./multicodec') + +const log = debug('libp2p:circuit:relay') +log.err = debug('libp2p:circuit:error:relay') + +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) + } + + /** + * The handler called to process a connection + * + * @param {Connection} conn + * @param {Multiaddr} dstAddr + * @param {Function} cb + * + * @return {void} + */ + 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 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) { + log.err(err) + return cb(err) + } + + let stream = handshake({timeout: 1000 * 60}, cb) + 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(), + 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 diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..ed941bc --- /dev/null +++ b/src/index.js @@ -0,0 +1,7 @@ +'use strict' + +module.exports = { + Circuit: require('./circuit-relay'), + CircuitDialer: require('./circuit-dialer'), + multicodec: require('./multicodec') +} diff --git a/src/listener.js b/src/listener.js new file mode 100644 index 0000000..7f98d43 --- /dev/null +++ b/src/listener.js @@ -0,0 +1,94 @@ +'use strict' + +const includes = require('lodash/includes') +const pull = require('pull-stream') +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 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, 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) + handler(null, newConn) + } + }) + + pull( + stream, + conn, + stream) + }) + }) + + listener.emit('listen') + cb() + } + + listener.close = (cb) => { + // TODO: should we close/abort connections here? + // spdy-transport throws 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) => { + 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..1d032a3 --- /dev/null +++ b/src/multicodec.js @@ -0,0 +1,3 @@ +'use strict' + +module.exports = '/ipfs/relay/circuit/1.0.0' diff --git a/src/peer.js b/src/peer.js new file mode 100644 index 0000000..d961ff9 --- /dev/null +++ b/src/peer.js @@ -0,0 +1,46 @@ +'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) + } + + close () { + this.conn = null + this.peerInfo = null + } +} + +module.exports = Peer diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..d3169da --- /dev/null +++ b/src/utils.js @@ -0,0 +1,34 @@ +'use strict' +const debug = require('debug') + +const log = debug('libp2p:circuit:utils') +log.err = debug('libp2p:circuit:error:utils') + +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 new file mode 100644 index 0000000..4af2604 --- /dev/null +++ b/test/index.spec.js @@ -0,0 +1,177 @@ +/* eslint-env mocha */ +'use strict' + +const PeerInfo = require('peer-info') +const series = require('async/series') +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 secio = require('libp2p-secio') + +const expect = require('chai').expect + +class TestNode extends Libp2p { + constructor (peerInfo, transports, muxer, options) { + options = options || {} + + const modules = { + transport: transports, + connection: { + muxer: [muxer], + crypto: [ + secio + ] + }, + discovery: [] + } + super(modules, peerInfo, null, options) + } +} + +describe('test relay', function () { + this.timeout(500000) + + let srcNode + let dstNode + let relayNode + + let srcPeer + let dstPeer + let relayPeer + + let portBase = 9000 // TODO: randomize or mock sockets + + 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()], 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) + } + + function startNodes (muxer, done) { + series([ + (cb) => setUpNodes(muxer, cb), + (cb) => { + relayNode.start(cb) + }, + (cb) => { + srcNode.start(cb) + }, + (cb) => { + dstNode.start(cb) + }, + (cb) => srcNode.dialByPeerInfo(relayPeer, cb), + (cb) => dstNode.dialByPeerInfo(relayPeer, cb) + ], done) + } + + function stopNodes (done) { + series([ + (cb) => { + srcNode.stop(cb) + }, + (cb) => { + dstNode.stop(cb) + }, + (cb) => { + relayNode.stop(cb) + } + ], () => 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) + + pull( + pull.values(['hello']), + conn, + pull.collect((err, data) => { + if (err) return done(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) { + dialAndRevers(['hello'], done) + }) + + 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 () { + 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) + }) + }) +})