From b2bd6c9bcab0ee2d0776d972c2ecc3c513522aa5 Mon Sep 17 00:00:00 2001 From: dmitriy ryajov Date: Tue, 21 Feb 2017 00:19:32 -0800 Subject: [PATCH 01/20] 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 befc025951d84c77b50545aedacea361aef4511d Mon Sep 17 00:00:00 2001 From: dmitriy ryajov Date: Tue, 21 Feb 2017 00:28:36 -0800 Subject: [PATCH 02/20] feat: initial implementation of circuit relaying chore: adding default readme feat: reworking as a transport feat: getting peers communicating over relay (wip) feat: address in swarm [wip] feat: adding onion dial feat: adding onion dialing and tests feat: make circuit a full fledged transport refactor: split transport dialer and circuit logic test: adding dial tests feat: adding passive/active dialing test test: adding relay tests fix: several isues feat: consolidate and cleanup dialing feat: handle listenning circuit addresses correctly feat: make utils a factory feat: adding StreamHandler to aid with pull-stream read/write refactor: clean up and refactor relay and listener tests: adding more relay and listener tests tests: moving long multiaddr to a fixture feat: adding _writeErr to handle returning errors in relay.js fix: cleanup, moving setup code outside of dialer --- .gitignore | 41 +++++++ .npmignore | 35 ++++++ .travis.yml | 38 +++++++ README.md | 40 ++++++- circle.yml | 12 ++ package.json | 66 +++++++++++ src/circuit/constants.js | 30 +++++ src/circuit/dialer.js | 186 +++++++++++++++++++++++++++++++ src/circuit/onion-dialer.js | 162 +++++++++++++++++++++++++++ src/circuit/relay.js | 200 ++++++++++++++++++++++++++++++++++ src/circuit/stream-handler.js | 101 +++++++++++++++++ src/circuit/utils.js | 74 +++++++++++++ src/dialer.js | 132 ++++++++++++++++++++++ src/index.js | 8 ++ src/listener.js | 126 +++++++++++++++++++++ src/multicodec.js | 6 + test/dialer.spec.js | 142 ++++++++++++++++++++++++ test/fixtures/long-address.js | 20 ++++ test/fixtures/nodes.js | 25 +++++ test/helpers/test-node.js | 22 ++++ test/helpers/utils.js | 110 +++++++++++++++++++ test/listener.spec.js | 166 ++++++++++++++++++++++++++++ test/onion-dialer.spec.js | 172 +++++++++++++++++++++++++++++ test/relay.spec.js | 130 ++++++++++++++++++++++ 24 files changed, 2042 insertions(+), 2 deletions(-) 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/circuit/constants.js create mode 100644 src/circuit/dialer.js create mode 100644 src/circuit/onion-dialer.js create mode 100644 src/circuit/relay.js create mode 100644 src/circuit/stream-handler.js create mode 100644 src/circuit/utils.js create mode 100644 src/dialer.js create mode 100644 src/index.js create mode 100644 src/listener.js create mode 100644 src/multicodec.js create mode 100644 test/dialer.spec.js create mode 100644 test/fixtures/long-address.js create mode 100644 test/fixtures/nodes.js create mode 100644 test/helpers/test-node.js create mode 100644 test/helpers/utils.js create mode 100644 test/listener.spec.js create mode 100644 test/onion-dialer.spec.js create mode 100644 test/relay.spec.js 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/.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/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/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..0359395 --- /dev/null +++ b/package.json @@ -0,0 +1,66 @@ +{ + "name": "libp2p-circuit", + "version": "0.0.2", + "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" + ], + "rules": { + "strict": "off" + } + }, + "devDependencies": { + "aegir": "^10.0.0", + "chai": "^3.5.0", + "multihashes": "^0.4.5", + "pre-commit": "^1.2.2", + "proxyquire": "^1.7.11", + "pull-pushable": "^2.0.1", + "sinon": "^2.1.0" + }, + "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", + "peer-id": "^0.8.2", + "peer-info": "^0.8.3", + "pull-abortable": "^4.1.0", + "pull-handshake": "^1.1.4", + "pull-stream": "^3.5.0", + "safe-buffer": "^5.0.1", + "setimmediate": "^1.0.5" + } +} diff --git a/src/circuit/constants.js b/src/circuit/constants.js new file mode 100644 index 0000000..876b479 --- /dev/null +++ b/src/circuit/constants.js @@ -0,0 +1,30 @@ +'use strict' + +module.exports = { + PRIOIRY: 100, + DIALER: { + ONION: 'onion', + TELESCOPE: 'telescope' + }, + RESPONSE: { + SUCCESS: 100, + FAILURE: 500, + HOP: { + SRC_ADDR_TOO_LONG: 220, + DST_ADDR_TOO_LONG: 221, + SRC_MULTIADDR_INVALID: 250, + DST_MULTIADDR_INVALID: 251, + NO_CONN_TO_DST: 260, + CANT_DIAL_DST: 261, + CANT_OPEN_DST_STREAM: 262, + CANT_SPEAK_RELAY: 270, + CANT_CONNECT_TO_SELF: 280 + }, + STOP: { + SRC_ADDR_TOO_LONG: 320, + DST_ADDR_TOO_LONG: 321, + SRC_MULTIADDR_INVALID: 350, + DST_MULTIADDR_INVALID: 351 + } + } +} diff --git a/src/circuit/dialer.js b/src/circuit/dialer.js new file mode 100644 index 0000000..b1fe822 --- /dev/null +++ b/src/circuit/dialer.js @@ -0,0 +1,186 @@ +'use strict' + +require('safe-buffer') + +const Connection = require('interface-connection').Connection +const isFunction = require('lodash.isfunction') +const multiaddr = require('multiaddr') +const constants = require('./constants') +const once = require('once') +const waterfall = require('async/waterfall') +const utilsFactory = require('./utils') +const StreamHandler = require('./stream-handler') + +const debug = require('debug') +const log = debug('libp2p:circuit:dialer') +log.err = debug('libp2p:circuit:error:dialer') + +const multicodec = require('../multicodec') + +class Dialer { + /** + * Creates an instance of Dialer. + * @param {Swarm} swarm - the swarm + * @param {any} options - config options + * + * @memberOf Dialer + */ + constructor (swarm, options) { + this.swarm = swarm + this.relayPeers = new Map() + this.options = options + this.utils = utilsFactory(swarm) + } + + /** + * 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) { + throw new Error('abstract class, method not implemented') + } + + /** + * Dial the destination peer over a relay + * + * @param {multiaddr} dstMa + * @param {Connection|PeerInfo} relay + * @param {Function} cb + * @return {Function|void} + * @private + */ + dialPeer (dstMa, relay, cb) { + if (isFunction(relay)) { + cb = relay + relay = null + } + + if (!cb) { + cb = () => {} + } + + dstMa = multiaddr(dstMa) + // if no relay provided, dial on all available relays until one succeeds + if (!relay) { + const relays = Array.from(this.relayPeers.values()) + let next = (nextRelay) => { + if (!nextRelay) { + let err = `no relay peers were found or all relays failed to dial` + log.err(err) + return cb(err) + } + + return this.negotiateRelay(nextRelay, dstMa, (err, conn) => { + if (err) { + log.err(err) + return next(relays.shift()) + } + cb(null, conn) + }) + } + next(relays.shift()) + } else { + return this.negotiateRelay(relay, dstMa, (err, conn) => { + if (err) { + log.err(`An error has occurred negotiating the relay connection`, err) + return cb(err) + } + + return cb(null, conn) + }) + } + } + + /** + * Negotiate the relay connection + * + * @param {Multiaddr|PeerInfo|Connection} relay - the Connection or PeerInfo of the relay + * @param {multiaddr} dstMa - the multiaddr of the peer to relay the connection for + * @param {Function} callback - a callback with that return the negotiated relay connection + * @returns {void} + * + * @memberOf Dialer + */ + negotiateRelay (relay, dstMa, callback) { + dstMa = multiaddr(dstMa) + + // TODO: whats the best addr to send? First one seems as good as any. + const srcMa = this.swarm._peerInfo.multiaddrs.toArray()[0] + const relayConn = new Connection() + + let streamHandler = null + waterfall([ + (cb) => { + if (relay instanceof Connection) { + return cb(null, relay) + } + return this.dialRelay(this.utils.peerInfoFromMa(relay), cb) + }, + (conn, cb) => { + log(`negotiating relay for peer ${dstMa.getPeerId()}`) + streamHandler = new StreamHandler(conn, 1000 * 60) + streamHandler.write([ + new Buffer(dstMa.toString()), + new Buffer(srcMa.toString()) + ], (err) => { + if (err) { + log.err(err) + return cb(err) + } + + cb(null) + }) + }, + (cb) => { + streamHandler.read((err, msg) => { + if (err) { + log.err(err) + return cb(err) + } + + if (Number(msg.toString()) !== constants.RESPONSE.SUCCESS) { + return cb(new Error(`Got ${msg.toString()} error code trying to dial over relay`)) + } + + relayConn.setInnerConn(streamHandler.rest()) + cb(null, relayConn) + }) + } + ], callback) + } + + /** + * Dial a relay peer by its PeerInfo + * + * @param {PeerInfo} peer - the PeerInfo of the relay peer + * @param {Function} cb - a callback with the connection to the relay peer + * @returns {Function|void} + * + * @memberOf Dialer + */ + dialRelay (peer, cb) { + cb = once(cb || (() => {})) + + const relayConn = new Connection() + relayConn.setPeerInfo(peer) + // attempt to dia the relay so that we have a connection + this.swarm.dial(peer, multicodec.hop, once((err, conn) => { + if (err) { + log.err(err) + return cb(err) + } + + relayConn.setInnerConn(conn) + this.relayPeers.set(this.utils.getB58String(peer), peer) + cb(null, relayConn) + })) + } +} + +module.exports = Dialer diff --git a/src/circuit/onion-dialer.js b/src/circuit/onion-dialer.js new file mode 100644 index 0000000..3961ba1 --- /dev/null +++ b/src/circuit/onion-dialer.js @@ -0,0 +1,162 @@ +'use strict' + +require('setimmediate') +require('safe-buffer') + +const Dialer = require('./dialer') +const isFunction = require('lodash.isfunction') +const multiaddr = require('multiaddr') +const Connection = require('interface-connection').Connection +const multicodec = require('../multicodec') + +const debug = require('debug') +const log = debug('libp2p:circuit:oniondialer') +log.err = debug('libp2p:circuit:error:oniondialer') + +class OnionDialer extends Dialer { + /** + * 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 maddrs = multiaddr(ma).toString().split('/p2p-circuit').filter((a) => a.length) + if (maddrs.length > 0) { + const id = multiaddr(maddrs[maddrs.length - 1]).getPeerId() + if (this.swarm._peerInfo.id.toB58String() === id) { + let err = `cant dial to self!` + log.err(err) + return cb(err) + } + } + + let dstConn = new Connection() + setImmediate(this._onionDial.bind(this), maddrs, (err, conn) => { + if (err) { + log.err(err) + return cb(err) + } + dstConn.setInnerConn(conn) + cb(null, dstConn) + }) + + return dstConn + } + + /** + * Dial a peer using onion dial - dial all relays in the ma + * in sequence and circuit dest over the all the pipes + * + * @param {multiaddr} maddrs + * @param {Connection} relay + * @param {Function} cb + * @return {void} + * @private + */ + _onionDial (maddrs, relay, cb) { + if (isFunction(relay)) { + cb = relay + relay = null + } + + const dial = (dstAddr, relayPeer) => { + if (maddrs.length) { + this._createRelayPipe(dstAddr, relayPeer, (err, upgraded) => { + if (err) { + log.err(err) + return cb(err) + } + return this._onionDial(maddrs, upgraded, cb) + }) + } else { + this.dialPeer(dstAddr, relayPeer, (err, conn) => { + if (err) { + return cb(err) + } + cb(null, conn) + }) + } + } + + if (maddrs.length >= 2) { + const relayAddr = multiaddr(maddrs.shift()) + const destAddr = multiaddr(maddrs.shift()) + dial(destAddr, relayAddr) + } else { + dial(multiaddr(maddrs.shift()), relay) + } + } + + /** + * Creates a relay connection that can be used explicitly from two multiaddrs + * + * @param {Multiaddr} dstAddr + * @param {Multiaddr} relayPeer + * @param {Function} cb + * @returns {void|Function} + * @private + */ + _createRelayPipe (dstAddr, relayPeer, cb) { + this.dialPeer(dstAddr, relayPeer, (err, conn) => { + if (err) { + log.err(err) + return cb(err) + } + + dstAddr = multiaddr(dstAddr) + this.relayPeers.set(dstAddr.getPeerId(), conn) + this._handshake(this.utils.peerInfoFromMa(dstAddr), conn, (err, upgraded) => { + if (err) { + log.err(err) + return cb(err) + } + + cb(null, upgraded) + }) + }) + } + + /** + * Upgrade a raw connection to a relayed link (hop) + * + * @param {PeerInfo} pi + * @param {Connection} conn + * @param {Function} cb + * + * @return {Function|void} + * @private + */ + _handshake (pi, conn, cb) { + const proxyConn = new Connection() + const handler = this.swarm.connHandler(pi, multicodec.hop, proxyConn) + handler.handleNew(conn, (err, upgradedConn) => { + if (err) { + log.err(err) + return cb(err) + } + + if (!upgradedConn) { + proxyConn.setInnerConn(conn) + upgradedConn = proxyConn + } + + cb(null, upgradedConn) + }) + } +} + +module.exports = OnionDialer diff --git a/src/circuit/relay.js b/src/circuit/relay.js new file mode 100644 index 0000000..650c3e9 --- /dev/null +++ b/src/circuit/relay.js @@ -0,0 +1,200 @@ +'use strict' + +require('setimmediate') +require('safe-buffer') + +const pull = require('pull-stream') +const debug = require('debug') +const PeerInfo = require('peer-info') +const PeerId = require('peer-id') +const EE = require('events').EventEmitter +const multiaddr = require('multiaddr') +const constants = require('./constants') +const once = require('once') +const utilsFactory = require('./utils') +const StreamHandler = require('./stream-handler') +const waterfall = require('async/waterfall') + +const multicodec = require('./../multicodec') + +const log = debug('libp2p:swarm:circuit:relay') +log.err = debug('libp2p:swarm:circuit:error:relay') + +let utils +class Relay extends EE { + /** + * 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 {any} options - configuration for Relay + */ + constructor (options) { + super() + this.config = Object.assign({active: false}, options) + this.swarm = null + this.active = this.config.active + } + + _writeErr (streamHandler, errCode, cb) { + errCode = String(errCode) + setImmediate(() => this.emit('circuit:error', errCode)) + streamHandler.write([Buffer.from(errCode)]) + return cb(errCode) + } + + _readSrcAddr (streamHandler, cb) { + streamHandler.read((err, srcMa) => { + if (err) { + log.err(err) + + // TODO: pull-length-prefixed should probably return an `Error` object with an error code + if (typeof err === 'string' && err.includes('size longer than max permitted length of')) { + return this._writeErr(streamHandler, constants.RESPONSE.HOP.SRC_ADDR_TOO_LONG, cb) + } + } + + try { + srcMa = multiaddr(srcMa.toString()) // read the src multiaddr + } catch (err) { + return this._writeErr(streamHandler, constants.RESPONSE.HOP.SRC_MULTIADDR_INVALID, cb) + } + + cb(null, srcMa) + }) + } + + _readDstAddr (streamHandler, cb) { + streamHandler.read((err, dstMa) => { + if (err) { + log.err(err) + + // TODO: pull-length-prefixed should probably return an `Error` object with an error code + if (typeof err === 'string' && err.includes('size longer than max permitted length of')) { + return this._writeErr(streamHandler, constants.RESPONSE.HOP.DST_ADDR_TOO_LONG, cb) + } + } + + try { + dstMa = multiaddr(dstMa.toString()) // read the src multiaddr + } catch (err) { + return this._writeErr(streamHandler, constants.RESPONSE.HOP.DST_MULTIADDR_INVALID, cb) + } + + if (dstMa.getPeerId() === this.swarm._peerInfo.id.toB58String()) { + return this._writeErr(streamHandler, constants.RESPONSE.HOP.CANT_CONNECT_TO_SELF, cb) + } + + if (!this.active && !utils.isPeerConnected(dstMa.getPeerId())) { + return this._writeErr(streamHandler, constants.RESPONSE.HOP.NO_CONN_TO_DST, cb) + } + + cb(null, dstMa) + }) + } + + /** + * Mount the relay + * + * @param {swarm} swarm + * @return {void} + */ + mount (swarm) { + this.swarm = swarm + utils = utilsFactory(swarm) + this.swarm.handle(multicodec.hop, (proto, conn) => { + const streamHandler = new StreamHandler(conn, 1000 * 60) + waterfall([ + (cb) => this._readDstAddr(streamHandler, cb), + (dstMa, cb) => { + this._readSrcAddr(streamHandler, (err, srcMa) => { + cb(err, dstMa, srcMa) + }) + } + ], (err, dstMa, srcMa) => { + if (err || (!dstMa || !srcMa)) { + log.err(`Error handling incoming relay request`, err) + return + } + + return this._circuit(streamHandler.rest(), dstMa, srcMa, (err) => { + if (err) { + log.err(err) + setImmediate(() => this.emit('circuit:error', err)) + } + setImmediate(() => this.emit('circuit:success')) + }) + }) + }) + + this.emit('mounted') + } + + /** + * The handler called to process a connection + * + * @param {Connection} conn + * @param {Multiaddr} dstAddr + * @param {Multiaddr} srcAddr + * @param {Function} cb + * + * @return {void} + */ + _circuit (conn, dstAddr, srcAddr, cb) { + this._dialPeer(dstAddr, (err, dstConn) => { + if (err) { + const errStreamHandler = new StreamHandler(conn, 1000 * 60) + this._writeErr(errStreamHandler, constants.RESPONSE.CANT_DIAL_DST) + pull(pull.empty(), errStreamHandler.rest()) + log.err(err) + return cb(err) + } + + const streamHandler = new StreamHandler(dstConn, 1000 * 60) + streamHandler.write([new Buffer(srcAddr.toString())], (err) => { + if (err) { + const errStreamHandler = new StreamHandler(conn, 1000 * 60) + this._writeErr(errStreamHandler, constants.RESPONSE.CANT_OPEN_DST_STREAM) + pull(pull.empty(), errStreamHandler.rest()) + + log.err(err) + return cb(err) + } + + // circuit the src and dst streams + pull( + conn, + streamHandler.rest(), + conn + ) + + cb() + }) + }) + } + + /** + * Dial the dest peer and create a circuit + * + * @param {Multiaddr} ma + * @param {Function} callback + * @returns {Function|void} + * @private + */ + _dialPeer (ma, callback) { + const peerInfo = new PeerInfo(PeerId.createFromB58String(ma.getPeerId())) + peerInfo.multiaddrs.add(ma) + this.swarm.dial(peerInfo, multicodec.stop, once((err, conn) => { + if (err) { + log.err(err) + return callback(err) + } + + callback(null, conn) + })) + } +} + +module.exports = Relay diff --git a/src/circuit/stream-handler.js b/src/circuit/stream-handler.js new file mode 100644 index 0000000..48fe675 --- /dev/null +++ b/src/circuit/stream-handler.js @@ -0,0 +1,101 @@ +'use strict' + +const pull = require('pull-stream') +const lp = require('pull-length-prefixed') +const handshake = require('pull-handshake') + +const debug = require('debug') +const log = debug('libp2p:circuit:stream-handler') +log.err = debug('libp2p:circuit:error:stream-handler') + +class StreamHandler { + /** + * Create a stream handler for connection + * + * @param {Connection} conn - connection to read/write + * @param {Number} [timeout] - handshake timeout + * @param {Number} [maxLength] - max bytes length of message + */ + constructor (conn, timeout, maxLength) { + this.conn = conn + this.stream = null + this.shake = null + this.maxLength = maxLength || 1024 + + this.stream = handshake({timeout: timeout || 1000 * 60}) + this.shake = this.stream.handshake + + pull(this.stream, conn, this.stream) + } + + isValid () { + return this.conn && this.shake && this.stream + } + + /** + * Read and decode message + * + * @param {Function} cb + * @returns {void|Function} + */ + read (cb) { + if (!this.isValid()) { + cb(new Error(`handler is not in a valid state`)) + } + + lp.decodeFromReader(this.shake, {maxLength: this.maxLength}, (err, msg) => { + if (err) { + log.err(err) + // this.shake.abort(err) + return cb(err) + } + + return cb(null, msg) + }) + } + + /** + * Encode and write array of buffers + * + * @param {Buffer[]} msg + * @param {Function} [cb] + * @returns {Function} + */ + write (msg, cb) { + cb = cb || (() => {}) + + if (!this.isValid()) { + cb(new Error(`handler is not in a valid state`)) + } + + pull( + pull.values(msg), + lp.encode(), + pull.collect((err, encoded) => { + if (err) { + log.err(err) + this.shake.abort(err) + } + + encoded.forEach((e) => this.shake.write(e)) + cb() + }) + ) + } + + /** + * Return the handshake rest stream and invalidate handler + * + * @return {*|{source, sink}} + */ + rest () { + const rest = this.shake.rest() + + this.conn = null + this.stream = null + this.shake = null + return rest + } +} + +module.exports = StreamHandler diff --git a/src/circuit/utils.js b/src/circuit/utils.js new file mode 100644 index 0000000..df2c6d3 --- /dev/null +++ b/src/circuit/utils.js @@ -0,0 +1,74 @@ +'use strict' + +const multiaddr = require('multiaddr') +const PeerInfo = require('peer-info') +const PeerId = require('peer-id') + +module.exports = function (swarm) { + /** + * Get b58 string from multiaddr or peerinfo + * + * @param {Multiaddr|PeerInfo} peer + * @return {*} + */ + function getB58String (peer) { + let b58Id = null + if (multiaddr.isMultiaddr(peer)) { + const relayMa = multiaddr(peer) + b58Id = relayMa.getPeerId() + } else if (PeerInfo.isPeerInfo(peer)) { + b58Id = peer.id.toB58String() + } + + return b58Id + } + + /** + * Helper to make a peer info from a multiaddrs + * + * @param {Multiaddr|PeerInfo|PeerId} ma + * @param {Swarm} swarm + * @return {PeerInfo} + * @private + */ + // TODO: this is ripped off of libp2p, should probably be a generally available util function + function peerInfoFromMa (peer) { + let p + // PeerInfo + if (PeerInfo.isPeerInfo(peer)) { + p = peer + // Multiaddr instance (not string) + } else if (multiaddr.isMultiaddr(peer)) { + const peerIdB58Str = peer.getPeerId() + try { + p = swarm._peerBook.get(peerIdB58Str) + } catch (err) { + p = new PeerInfo(PeerId.createFromB58String(peerIdB58Str)) + } + p.multiaddrs.add(peer) + // PeerId + } else if (PeerId.isPeerId(peer)) { + const peerIdB58Str = peer.toB58String() + p = swarm._peerBook.has(peerIdB58Str) ? swarm._peerBook.get(peerIdB58Str) : peer + } + + return p + } + + /** + * Checks if peer has an existing connection + * + * @param {String} peerId + * @param {Swarm} swarm + * @return {Boolean} + */ + function isPeerConnected (peerId) { + return swarm.muxedConns[peerId] || swarm.conns[peerId] + } + + return { + getB58String: getB58String, + peerInfoFromMa: peerInfoFromMa, + isPeerConnected: isPeerConnected + } +} diff --git a/src/dialer.js b/src/dialer.js new file mode 100644 index 0000000..d44ba7a --- /dev/null +++ b/src/dialer.js @@ -0,0 +1,132 @@ +'use strict' + +const mafmt = require('mafmt') +const multiaddr = require('multiaddr') + +const constants = require('./circuit/constants') +const OnionDialer = require('./circuit/onion-dialer') +const utilsFactory = require('./circuit/utils') + +const debug = require('debug') +const log = debug('libp2p:circuit:transportdialer') +log.err = debug('libp2p:circuit:error:transportdialer') + +const createListener = require('./listener') + +class Dialer { + /** + * Creates an instance of Dialer. + * @param {Swarm} swarm - the swarm + * @param {any} options - config options + * + * @memberOf Dialer + */ + constructor (swarm, options) { + options = options || {} + + this.swarm = swarm + this.dialer = null + this.utils = utilsFactory(swarm) + + // get all the relay addresses for this swarm + const relays = this.filter(swarm._peerInfo.multiaddrs.toArray()) + + // if no explicit relays, add a default relay addr + if (relays.length === 0) { + this.swarm + ._peerInfo + .multiaddrs + .add(`/p2p-circuit/ipfs/${this.swarm._peerInfo.id.toB58String()}`) + } + + // TODO: add flag for other types of dealers, ie telescope + this.dialer = new OnionDialer(swarm, options) + + this.swarm.on('peer-mux-established', this.dialer.dialRelay.bind(this.dialer)) + this.swarm.on('peer-mux-closed', (peerInfo) => { + this.dialer.relayPeers.delete(peerInfo.id.toB58String()) + }) + + this._dialSwarmRelays(relays) + } + + /** + * Dial the relays in the Addresses.Swarm config + * + * @param {Array} relays + * @return {void} + */ + _dialSwarmRelays (relays) { + // if we have relay addresses in swarm config, then dial those relays + this.swarm.on('listening', () => { + relays.forEach((relay) => { + let relaySegments = relay + .toString() + .split('/p2p-circuit') + .filter(segment => segment.length) + + relaySegments.forEach((relaySegment) => { + this.dialer.dialRelay(this.utils.peerInfoFromMa(multiaddr(relaySegment))) + }) + }) + }) + } + + get priority () { + return constants.PRIOIRY // TODO: move to a constants file that all transports can share + } + + set priority (val) { + throw new Error('Priority is read only!') + } + + /** + * 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) { + return this.dialer.dial(ma, options, cb) + } + + /** + * Create a listener + * + * @param {any} options + * @param {Function} handler + * @return {listener} + */ + createListener (options, handler) { + if (typeof options === 'function') { + handler = options + options = this.options || {} + } + + return createListener(this.swarm, options, handler) + } + + /** + * 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 new file mode 100644 index 0000000..92a3667 --- /dev/null +++ b/src/index.js @@ -0,0 +1,8 @@ +'use strict' + +module.exports = { + Relay: require('./circuit/relay'), + Dialer: require('./dialer'), + multicodec: require('./multicodec'), + tag: 'Circuit' +} diff --git a/src/listener.js b/src/listener.js new file mode 100644 index 0000000..7017110 --- /dev/null +++ b/src/listener.js @@ -0,0 +1,126 @@ +'use strict' + +require('safe-buffer') +require('setimmediate') + +const multicodec = require('./multicodec') +const EE = require('events').EventEmitter +const multiaddr = require('multiaddr') +const Connection = require('interface-connection').Connection +const mafmt = require('mafmt') +const constants = require('./circuit/constants') +const waterfall = require('async/waterfall') +const StreamHandler = require('./circuit/stream-handler') + +const debug = require('debug') + +const log = debug('libp2p:circuit:listener') +log.err = debug('libp2p:circuit:error:listener') + +module.exports = (swarm, options, handler) => { + const listener = new EE() + + listener.listen = (ma, callback) => { + callback = callback || (() => {}) + + swarm.handle(multicodec.stop, (proto, conn) => { + conn.getPeerInfo((err, peerInfo) => { + if (err) { + log.err('Failed to identify incoming connection', err) + return handler(err, null) + } + + const streamHandler = new StreamHandler(conn) + waterfall([ + (cb) => { + streamHandler.read((err, msg) => { + if (err) { + log.err(err) + + if (err.includes('size longer than max permitted length of')) { + const errCode = String(constants.RESPONSE.STOP.SRC_ADDR_TOO_LONG) + setImmediate(() => this.emit('circuit:error', errCode)) + streamHandler.write([Buffer.from(errCode)]) + } + + return cb(err) + } + + let srcMa = null + try { + srcMa = multiaddr(msg.toString()) + } catch (err) { + const errCode = String(constants.RESPONSE.STOP.SRC_MULTIADDR_INVALID) + setImmediate(() => this.emit('circuit:error', errCode)) + streamHandler.write([Buffer.from(errCode)]) + return cb(errCode) + } + + // add the addr we got along with the relay request + peerInfo.multiaddrs.add(srcMa) + cb() + }) + }, + (cb) => { + streamHandler.write([Buffer.from(String(constants.RESPONSE.SUCCESS))], (err) => { + if (err) { + log.err(err) + return cb(err) + } + + const newConn = new Connection(streamHandler.rest(), conn) + newConn.setPeerInfo(peerInfo) + setImmediate(() => listener.emit('connection', newConn)) + handler(newConn) + cb() + }) + } + ]) + }) + }) + + setImmediate(() => listener.emit('listen')) + callback() + } + + listener.close = (cb) => { + swarm.unhandle(multicodec.stop) + setImmediate(() => listener.emit('close')) + cb() + } + + listener.getAddrs = (callback) => { + let addrs = swarm._peerInfo.multiaddrs.toArray() + + // get all the explicit relay addrs excluding self + let p2pAddrs = addrs.filter((addr) => { + return mafmt.Circuit.matches(addr) && + !addr.toString().includes(swarm._peerInfo.id.toB58String()) + }) + + // use the explicit relays instead of any relay + if (p2pAddrs.length) { + addrs = p2pAddrs + } + + let listenAddrs = [] + addrs.forEach((addr) => { + const peerMa = `/p2p-circuit/ipfs/${swarm._peerInfo.id.toB58String()}` + if (addr.toString() === peerMa) { + listenAddrs.push(multiaddr(peerMa)) + return + } + + if (!mafmt.Circuit.matches(addr)) { + // by default we're reachable over any relay + listenAddrs.push(multiaddr(`/p2p-circuit`).encapsulate(`${addr}/ipfs/${swarm._peerInfo.id.toB58String()}`)) + } else { + listenAddrs.push(addr.encapsulate(`/ipfs/${swarm._peerInfo.id.toB58String()}`)) + } + }) + + callback(null, listenAddrs) + } + + return listener +} diff --git a/src/multicodec.js b/src/multicodec.js new file mode 100644 index 0000000..3055325 --- /dev/null +++ b/src/multicodec.js @@ -0,0 +1,6 @@ +'use strict' + +module.exports = { + hop: '/ipfs/relay/circuit/1.0.0/hop', + stop: '/ipfs/relay/circuit/1.0.0/stop' +} diff --git a/test/dialer.spec.js b/test/dialer.spec.js new file mode 100644 index 0000000..c55d7c2 --- /dev/null +++ b/test/dialer.spec.js @@ -0,0 +1,142 @@ +/* eslint-env mocha */ +'use strict' + +const Dialer = require('../src/circuit/dialer') +const nodes = require('./fixtures/nodes') +const Connection = require('interface-connection').Connection +const multiaddr = require('multiaddr') +const constants = require('../src/circuit/constants') +const handshake = require('pull-handshake') +const PeerInfo = require('peer-info') +const PeerId = require('peer-id') +const waterfall = require('async/waterfall') +const pull = require('pull-stream') +const lp = require('pull-length-prefixed') + +const sinon = require('sinon') +const expect = require('chai').expect + +describe('dialer tests', function () { + describe('dialPeer', function () { + const dialer = sinon.createStubInstance(Dialer) + + beforeEach(function () { + dialer.relayPeers = new Map() + dialer.relayPeers.set(nodes.node1.id, new Connection()) + dialer.relayPeers.set(nodes.node2.id, new Connection()) + dialer.relayPeers.set(nodes.node3.id, new Connection()) + dialer.dialPeer.callThrough() + }) + + afterEach(function () { + dialer.negotiateRelay.reset() + }) + + it(`negotiate a relay on all available relays`, function (done) { + const dstMa = multiaddr(`/ipfs/${nodes.node4.id}`) + dialer.negotiateRelay.callsFake(function (conn, dstMa, callback) { + if (conn === dialer.relayPeers.get(nodes.node3.id)) { + return callback(null, dialer.relayPeers.get(nodes.node3.id)) + } + + callback('error') + }) + + dialer.dialPeer(dstMa, (err, conn) => { + expect(err).to.be.null + expect(conn).to.be.an.instanceOf(Connection) + expect(conn).to.deep.equal(dialer.relayPeers.get(nodes.node3.id)) + done() + }) + }) + + it(`try all available relays and fail with error if none succeed`, function (done) { + const dstMa = multiaddr(`/ipfs/${nodes.node4.id}`) + dialer.negotiateRelay.callsFake(function (conn, dstMa, callback) { + callback('error') + }) + + dialer.dialPeer(dstMa, (err, conn) => { + expect(conn).to.be.undefined + expect(err).to.not.be.null + expect(err).to.equal(`no relay peers were found or all relays failed to dial`) + done() + }) + }) + }) + + describe('negotiateRelay', function () { + const dialer = sinon.createStubInstance(Dialer) + const dstMa = multiaddr(`/ipfs/${nodes.node4.id}`) + + let conn + let stream + let shake + let callback = sinon.stub() + + beforeEach(function (done) { + waterfall([ + (cb) => PeerId.createFromJSON(nodes.node4, cb), + (peerId, cb) => PeerInfo.create(peerId, cb), + (peer, cb) => { + peer.multiaddrs.add('/p2p-circuit/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE') + dialer.swarm = { + _peerInfo: peer + } + cb() + }, + (cb) => { + dialer.relayConns = new Map() + dialer.negotiateRelay.callThrough() + stream = handshake({timeout: 1000 * 60}) + shake = stream.handshake + conn = new Connection() + conn.setPeerInfo(new PeerInfo(PeerId.createFromB58String('QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE'))) + conn.setInnerConn(stream) + dialer.negotiateRelay(conn, dstMa, callback) + cb() + } + ], done) + }) + + afterEach(() => { + callback.reset() + }) + + it(`write the correct dst addr`, function (done) { + lp.decodeFromReader(shake, (err, msg) => { + shake.write(new Buffer(String(constants.RESPONSE.SUCCESS))) + expect(err).to.be.null + expect(msg.toString()).to.be.equal(`${dstMa.toString()}`) + done() + }) + }) + + it(`fail negotiating relay`, function (done) { + callback.callsFake((err, msg) => { + expect(err).to.not.be.null + expect(err).to.be.an.instanceOf(Error) + expect(err.message).to.be.equal(`Got 500 error code trying to dial over relay`) + expect(callback.calledOnce).to.be.ok + done() + }) + + lp.decodeFromReader(shake, (err, msg) => { + if (err) return done(err) + + pull( + pull.values([Buffer.from(String(constants.RESPONSE.FAILURE))]), + lp.encode(), + pull.collect((err, encoded) => { + expect(err).to.be.null + encoded.forEach((e) => shake.write(e)) + }) + ) + }) + }) + }) + + // describe('dialRelay', function () { + // + // }) +}) diff --git a/test/fixtures/long-address.js b/test/fixtures/long-address.js new file mode 100644 index 0000000..543de67 --- /dev/null +++ b/test/fixtures/long-address.js @@ -0,0 +1,20 @@ +'use strict' + +module.exports = Buffer.from( + `/ip4/0.0.0.0/tcp/9033/ws/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE/p2p-circuit` + + `/ip4/0.0.0.0/tcp/9033/ws/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE/p2p-circuit` + + `/ip4/0.0.0.0/tcp/9033/ws/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE/p2p-circuit` + + `/ip4/0.0.0.0/tcp/9033/ws/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE/p2p-circuit` + + `/ip4/0.0.0.0/tcp/9033/ws/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE/p2p-circuit` + + `/ip4/0.0.0.0/tcp/9033/ws/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE/p2p-circuit` + + `/ip4/0.0.0.0/tcp/9033/ws/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE/p2p-circuit` + + `/ip4/0.0.0.0/tcp/9033/ws/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE/p2p-circuit` + + `/ip4/0.0.0.0/tcp/9033/ws/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE/p2p-circuit` + + `/ip4/0.0.0.0/tcp/9033/ws/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE/p2p-circuit` + + `/ip4/0.0.0.0/tcp/9033/ws/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE/p2p-circuit` + + `/ip4/0.0.0.0/tcp/9033/ws/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE/p2p-circuit` + + `/ip4/0.0.0.0/tcp/9033/ws/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE/p2p-circuit` + + `/ip4/0.0.0.0/tcp/9033/ws/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE/p2p-circuit` + + `/ip4/0.0.0.0/tcp/9033/ws/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE/p2p-circuit` + + `/ip4/0.0.0.0/tcp/9033/ws/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE/p2p-circuit` + + `/ip4/0.0.0.0/tcp/9033/ws/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE/p2p-circuit`) diff --git a/test/fixtures/nodes.js b/test/fixtures/nodes.js new file mode 100644 index 0000000..71a274d --- /dev/null +++ b/test/fixtures/nodes.js @@ -0,0 +1,25 @@ +'use strict' + +exports.node1 = { + id: 'QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE', + privKey: 'CAASpwkwggSjAgEAAoIBAQDJwzJPar4nylKY71Mm5q2BOED8uPf1ILvIi15VwVZWqter6flnlii/RKEcBypPbFqJHHa56MvybgQgrFmHKwDjnJvq4jyOZfR+o/D/99Ft1p2FAEBjImSXAgNpK4YsbyV5r0Q1+Avcj++aWWlLu6enUrL9WGzeUkf0U5L6XwXEPRUQdEojAIQi241P1hyqXX5gKAZVGqcPtKb6p1db3fcXodkS1G6JR90TopJHCqTCECp3SB9c6LlG7KXU92sIHJBlhOEEzGkEI1pM1SWnNnW5VLEypU7P56ifzzp4QxPNiJeC+cmE5SrgR3cXP44iKOuNVRJwBpCh5oNYqECzgqJ9AgMBAAECggEBAJpCdqXHrAmKJCqv2HiGqCODGhTfax1s4IYNIJwaTOPIjUrwgfKUGSVb2H4wcEX3RyVLsO6lMcFyIg/FFlJFK9HavE8SmFAbXZqxx6I9HE+JZjf5IEFrW1Mlg+wWDejNNe7adSF6O79wATaWo+32VNGWZilTQTGd4UvJ1jc9DZCh8zZeNhm4C6exXD45gMB0HI1t2ZNl47scsBEE4rV+s7F7y8Yk/tIsf0wSI/H8KSXS5I9aFxr3Z9c3HOfbVwhnIfNUDqcFTeU5BnhByYNLJ4v9xGj7puidcabVXkt2zLmm/LHbKVeGzec9LW5D+KkuB/pKaslsCXN6bVlu+SbVr9UCgYEA7MXfzZw36vDyfn4LPCN0wgzz11uh3cm31QzOPlWpA7hIsL/eInpvc8wa9yBRC1sRk41CedPHn913MR6EJi0Ne6/B1QOmRYBUjr60VPRNdTXCAiLykjXg6+TZ+AKnxlUGK1hjTo8krhpWq7iD/JchVlLoqDAXGFHvSxN0H3WEUm8CgYEA2iWC9w1v+YHfT2PXcLxYde9EuLVkIS4TM7Kb0N3wr/4+K4xWjVXuaJJLJoAbihNAZw0Y+2s1PswDUEpSG0jXeNXLs6XcQxYSEAu/pFdvHFeg2BfwVQoeEFlWyTJR29uti9/APaXMo8FSVAPPR5lKZLStJDM9hEfAPfUaHyic39MCgYAKQbwjNQw7Ejr+/cjQzxxkt5jskFyftfhPs2FP0/ghYB9OANHHnpQraQEWCYFZQ5WsVac2jdUM+NQL/a1t1e/Klt+HscPHKPsAwAQh1f9w/2YrH4ZwjQL0VRKYKs1HyzEcOZT7tzm4jQ2KHNEi5Q0dpzPK7WJivFHoZ6xVHIsh4wKBgAQq20mk9BKsLHvzyFXbA0WdgI6WyIbpvmwqaVegJcz26nEiiTTCA3/z64OcxunoXD6bvXJwJeBBPX73LIJg7dzdGLsh3AdcEJRF5S9ajEDaW7RFIM4/FzvwuPu2/mFY3QPjDmUfGb23H7+DIx6XCxjJatVaNT6lsEJ+wDUALZ8JAoGAO0YJSEziA7y0dXPK5azkJUMJ5yaN+zRDmoBnEggza34rQW0s16NnIR0EBzKGwbpNyePlProv4dQEaLF1kboKsSYvV2rW2ftLVdNqBHEUYFRC9ofPctCxwM1YU21TI2/k1squ+swApg2EHMev2+WKd+jpVPIbCIvJ3AjiAKZtiGQ=', + pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDJwzJPar4nylKY71Mm5q2BOED8uPf1ILvIi15VwVZWqter6flnlii/RKEcBypPbFqJHHa56MvybgQgrFmHKwDjnJvq4jyOZfR+o/D/99Ft1p2FAEBjImSXAgNpK4YsbyV5r0Q1+Avcj++aWWlLu6enUrL9WGzeUkf0U5L6XwXEPRUQdEojAIQi241P1hyqXX5gKAZVGqcPtKb6p1db3fcXodkS1G6JR90TopJHCqTCECp3SB9c6LlG7KXU92sIHJBlhOEEzGkEI1pM1SWnNnW5VLEypU7P56ifzzp4QxPNiJeC+cmE5SrgR3cXP44iKOuNVRJwBpCh5oNYqECzgqJ9AgMBAAE=' +} + +exports.node2 = { + id: 'QmYJjAri5soV8RbeQcHaYYcTAYTET17QTvcoFMyKvRDTXe', + privKey: 'CAASpgkwggSiAgEAAoIBAQDt7YgUeBQsoN/lrgo690mB7yEh8G9iXhZiDecgZCLRRSl3v2cH9w4WjhoW9erfnVbdoTqkCK+se8uK01ySi/ubQQDPcrjacXTa6wAuRTbCG/0bUR9RxKtxZZBS1HaY7L923ulgGDTiVaRQ3JQqhzmQkaU0ikNcluSGaw0kmhXP6JmcL+wndKgW5VD9etcp2Qlk8uUFC/GAO90cOAuER3wnI3ocHGm9on9zyb97g4TDzIfjSaTW4Wanmx2yVbURQxmCba16X3LT9IMPqQaGOzq3+EewMLeCESbUm/uJaJLdqWrWRK4oNzxcMgmUkzav+s476HdA9CRo72am+g3Vdq+lAgMBAAECggEAcByKD6MZVoIjnlVo6qoVUA1+3kAuK/rLrz5/1wp4QYXGaW+eO+mVENm6v3D3UJESGnLbb+nL5Ymbunmn2EHvuBNkL1wOcJgfiPxM5ICmscaAeHu8N0plwpQp8m28yIheG8Qj0az2VmQmfhfCFVwMquuGHgC8hwdu/Uu6MLIObx1xjtaGbY9kk7nzAeXHeJ4RDeuNN0QrYuQVKwrIz1NtPNDR/cli298ZXJcm+HEhBCIHVIYpAq6BHSuiXVqPGEOYWYXo+yVhEtDJ8BmNqlN1Y1s6bnfu/tFkKUN6iQQ46vYnQEGTGR9lg7J/c6tqfRs9FcywWb9J1SX6HxPO8184zQKBgQD6vDYl20UT4ZtrzhFfMyV/1QUqFM/TdwNuiOsIewHBol9o7aOjrxrrbYVa1HOxETyBjmFsW+iIfOVl61SG2HcU4CG+O2s9WBo4JdRlOm4YQ8/83xO3YfbXzuTx8BMCyP/i1uPIZTKQFFAN0HiL96r4L60xHoWB7tQsbZiEbIO/2wKBgQDy7HnkgVeTld6o0+sT84FYRUotjDB00oUWiSeGtj0pFC4yIxhMhD8QjKiWoJyJItcoCsQ/EncuuwwRtuXi83793lJQR1DBYd+TSPg0M8J1pw97fUIPi/FU+jHtrsx7Vn/7Bk9voictsYVLAfbi68tYdsZpAaYOWYMY9NUfVuAmfwKBgCYZDwk1hgt9TkZVK2KRvPLthTldrC5veQAEoeHJ/vxTFbg105V9d9Op8odYnLOc8NqmrbrvRCfpAlo4JcHPhliPrdDf6m2Jw4IgjWNMO4pIU4QSyUYmBoHIGBWC6wCTVf47tKSwa7xkub0/nfF2km3foKtD/fk+NtMBXBlS+7ndAoGAJo6GIlCtN82X07AfJcGGjB4jUetoXYJ0gUkvruAKARUk5+xOFQcAg33v3EiNz+5pu/9JesFRjWc+2Sjwf/8p7t10ry1Ckg8Yz2XLj22PteDYQj91VsZdfaFgf1s5NXJbSdqMjSltkoEUqP0c1JOcaOQhRdVvJ+PpPPLPSPQfC70CgYBvJE1I06s7BEM1DOli3VyfNaJDI4k9W2dCJOU6Bh2MNmbdRjM3xnpOKH5SqRlCz/oI9pn4dxgbX6WPg331MD9CNYy2tt5KBQRrSuDj8p4jlzMIpX36hsyTTrzYU6WWSIPz6jXW8IexXKvXEmr8TVb78ZPiQfbG012cdUhAJniNgg==', + pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDt7YgUeBQsoN/lrgo690mB7yEh8G9iXhZiDecgZCLRRSl3v2cH9w4WjhoW9erfnVbdoTqkCK+se8uK01ySi/ubQQDPcrjacXTa6wAuRTbCG/0bUR9RxKtxZZBS1HaY7L923ulgGDTiVaRQ3JQqhzmQkaU0ikNcluSGaw0kmhXP6JmcL+wndKgW5VD9etcp2Qlk8uUFC/GAO90cOAuER3wnI3ocHGm9on9zyb97g4TDzIfjSaTW4Wanmx2yVbURQxmCba16X3LT9IMPqQaGOzq3+EewMLeCESbUm/uJaJLdqWrWRK4oNzxcMgmUkzav+s476HdA9CRo72am+g3Vdq+lAgMBAAE=' +} + +exports.node3 = { + id: 'QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA', + privKey: 'CAASpwkwggSjAgEAAoIBAQDdnGp0X7Pix5dIawfyuffVryRDRS5JXdyjayKUkgikJLYoiijB5TakrFKhx1SDKpmVLxxqAGz8m5iA2cHwetIQXTZvdYx7XXxv332En3ji8TiGRUiEFM8KQ5WCJ5G7yw8R2pv/pYdnMrPd04QbtSCn0cFVCiiA2Zkl5KnwBo/lf+sVI/TEeiwmVD9nxi13qWgBTmCysqH8Ppyu8fq+bQgqRZSlalVDswyIhgWlepPkD0uYakJJhhOxY+2RlbNhGY0qjRyMTYou2uR/hfd6j8uR++WdB0v3+DYWG2Kc3sWa4BLYb5r4trvQGO1Iagnwuk3AVoi7PldsaInekzWEVljDAgMBAAECggEAXx0jE49/xXWkmJBXePYYSL5C8hxfIV4HtJvm251R2CFpjTy/AXk/Wq4bSRQkUaeXA1CVAWntXP3rFmJfurb8McnP80agZNJa9ikV1jYbzEt71yUlWosT0XPwV0xkYBVnAmKxUafZ1ZENYcfGi53RxjVgpP8XIzZBZOIfjcVDPVw9NAOzQmq4i3DJEz5xZAkaeSM8mn5ZFl1JMBUOgyOHB7d4BWd3zuLyvnn0/08HlsaSUl0mZa3f2Lm2NlsjOiNfMCJTOIT+xDEP9THm5n2cqieSjvtpAZzV4kcoD0rB8OsyHQlFAEXzkgELDr5dVXji0rrIdVz8stYAKGfi996OAQKBgQDuviV1sc+ClJQA59vqbBiKxWqcuCKMzvmL4Yk1e/AkQeRt+JX9kALWzBx65fFmHTj4Lus8AIQoiruPxa0thtqh/m3SlucWnrdaW410xbz3KqQWS7bx+0sFWZIEi4N+PESrIYhtVbFuRiabYgliqdSU9shxtXXnvfhjl+9quZltiwKBgQDtoUCKqrZbm0bmzLvpnKdNodg1lUHaKGgEvWgza2N1t3b/GE07iha2KO3hBDta3bdfIEEOagY8o13217D0VIGsYNKpiEGLEeNIjfcXBEqAKiTfa/sXUfTprpWBZQ/7ZS+eZIYtQjq14EHa7ifAby1v3yDrMIuxphz5JfKdXFgYqQKBgHr47FikPwu2tkmFJCyqgzWvnEufOQSoc7eOc1tePIKggiX2/mM+M4gqWJ0hJeeAM+D6YeZlKa2sUBItMxeZN7JrWGw5mEx5cl4TfFhipgP2LdDiLRiVZL4bte+rYQ67wm8XdatDkYIIlkhBBi6Q5dPZDcQsQNAedPvvvb2OXi4jAoGBAKp06FpP+L2fle2LYSRDlhNvDCvrpDA8mdkEkRGJb/AKKdb09LnH5WDH3VNy+KzGrHoVJfWUAmNPAOFHeYzabaZcUeEAd5utui7afytIjbSABrEpwRTKWneiH2aROzSnMdBZ5ZHjlz/N3Q+RlHxKg/piwTdUPHCzasch/HX6vsr5AoGAGvhCNPKyCwpu8Gg5GQdx0yN6ZPar9wieD345cLanDZWKkLRQbo4SfkfoS+PDfOLzDbWFdPRnWQ0qhdPm3D/N1YD/nudHqbeDlx0dj/6lEHmmPKFFO2kiNFEhn8DycNGbvWyVBKksacuRXav21+LvW+TatUkRMhi8fgRoypnbJjg=', + pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDdnGp0X7Pix5dIawfyuffVryRDRS5JXdyjayKUkgikJLYoiijB5TakrFKhx1SDKpmVLxxqAGz8m5iA2cHwetIQXTZvdYx7XXxv332En3ji8TiGRUiEFM8KQ5WCJ5G7yw8R2pv/pYdnMrPd04QbtSCn0cFVCiiA2Zkl5KnwBo/lf+sVI/TEeiwmVD9nxi13qWgBTmCysqH8Ppyu8fq+bQgqRZSlalVDswyIhgWlepPkD0uYakJJhhOxY+2RlbNhGY0qjRyMTYou2uR/hfd6j8uR++WdB0v3+DYWG2Kc3sWa4BLYb5r4trvQGO1Iagnwuk3AVoi7PldsaInekzWEVljDAgMBAAE=' +} + +exports.node4 = { + id: 'QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy', + privKey: 'CAASqAkwggSkAgEAAoIBAQC6pg6LYWbY+49SOYdYap6RPqKqZxg80IXeo3hiTUbiGtTruxVYZpnz3UbernL9J9mwlXJGRUQJUKmXmi1yePTQiyclpH0KyPefaWLbpxJQdCBI1TPZpDWo2hutWSPqhKBU1QyH2FLKQPWLdxuIX1cNFPtIlSl5gCxN6oiDIwh7++kxNM1G+d5XgJX6iHLlLoNv3Wn6XYX+VtYdyZRFk8gYyT2BrISbxmsrSjSOodwUUzF8TYTjsqW6ksL2x0mrRm2cMM9evULktqwU+I8l9ulASDbFWBXUToXaZSL9M+Oq5JvZO0WIjPeYVpAgWladtayhdxg5dBv8aTbDaM5DZvyRAgMBAAECggEAR65YbZz1k6Vg0HI5kXI4/YzxicHYJBrtHqjnJdGJxHILjZCmzPFydJ5phkG29ZRlXRS381bMn0s0Jn3WsFzVoHWgjitSvl6aAsXFapgKR42hjHcc15vh47wH3xYZ3gobTRkZG96vRO+XnX0bvM7orqR9MM3gRMI9wZqt3LcKnhpiqSlyEZ3Zehu7ZZ8B+XcUw42H6ZTXgmg5mCFEjS/1rVt+EsdZl7Ll7jHigahPA6qMjyRiZB6T20qQ0FFYfmaNuRuuC6cWUXf8DOgnEjMB/Mi/Feoip9bTqNBrVYn2XeDxdMv5pDznNKXpalsMkZwx5FpNOMKnIMdQFyAGtkeQ9QKBgQD3rjTiulitpbbQBzF8VXeymtMJAbR1TAqNv2yXoowhL3JZaWICM7nXHjjsJa3UzJygbi8bO0KWrw7tY0nUbPy5SmHtNYhmUsEjiTjqEnNRrYN68tEKr0HlgX+9rArsjOcwucl2svFSfk+rTYDHU5neZkDDhu1QmnZm/pQI92Lo4wKBgQDA6wpMd53fmX9DhWegs3xelRStcqBTw1ucWVRyPgY1hO1cJ0oReYIXKEw9CHNLW0RHvnVM26kRnqCl+dTcg7dhLuqrckuyQyY1KcRYG1ryJnz3euucaSF2UCsZCHvFNV7Vz8dszUMUVCogWmroVP6HE/BoazUCNh25s/dNwE+i+wKBgEfa1WL1luaBzgCaJaQhk4FQY2sYgIcLEYDACTwQn0C9aBpCdXmYEhEzpmX0JHM5DTOJ48atsYrPrK/3/yJOoB8NUk2kGzc8SOYLWGSoB6aphRx1N2o3IBH6ONoJAH5R/nxnWehCz7oUBP74lCS/v0MDPUS8bzrUJQeKUd4sDxjrAoGBAIRO7rJA+1qF+J1DWi4ByxNHJXZLfh/UhPj23w628SU1dGDWZVsUvZ7KOXdGW2RcRLj7q5E5uXtnEoCillViVJtnRPSun7Gzkfm2Gn3ezQH0WZKVkA+mnpd5JgW2JsS69L6pEPnS0OWZT4b+3AFZgXL8vs2ucR2CJeLdxYdilHuPAoGBAPLCzBkAboXZZtvEWqzqtVNqdMrjLHihFrpg4TXSsk8+ZQZCVN+sRyTGTvBX8+Jvx4at6ClaSgT3eJ/412fEH6CHvrFXjUE9W9y6X0axxaT63y1OXmFiB/hU3vjLWZKZWSDGNS7St02fYri4tWmGtJDjYG1maLRhMSzcoj4fP1xz', + pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6pg6LYWbY+49SOYdYap6RPqKqZxg80IXeo3hiTUbiGtTruxVYZpnz3UbernL9J9mwlXJGRUQJUKmXmi1yePTQiyclpH0KyPefaWLbpxJQdCBI1TPZpDWo2hutWSPqhKBU1QyH2FLKQPWLdxuIX1cNFPtIlSl5gCxN6oiDIwh7++kxNM1G+d5XgJX6iHLlLoNv3Wn6XYX+VtYdyZRFk8gYyT2BrISbxmsrSjSOodwUUzF8TYTjsqW6ksL2x0mrRm2cMM9evULktqwU+I8l9ulASDbFWBXUToXaZSL9M+Oq5JvZO0WIjPeYVpAgWladtayhdxg5dBv8aTbDaM5DZvyRAgMBAAE=' +} diff --git a/test/helpers/test-node.js b/test/helpers/test-node.js new file mode 100644 index 0000000..998bf07 --- /dev/null +++ b/test/helpers/test-node.js @@ -0,0 +1,22 @@ +'use strict' + +const Libp2p = require('libp2p') +const secio = require('libp2p-secio') + +class TestNode extends Libp2p { + constructor (peerInfo, transports, muxer, options) { + options = options || {} + + const modules = { + transport: transports, + connection: { + muxer: [muxer], + crypto: options.isCrypto ? [secio] : null + }, + discovery: [] + } + super(modules, peerInfo, null, options) + } +} + +module.exports = TestNode diff --git a/test/helpers/utils.js b/test/helpers/utils.js new file mode 100644 index 0000000..de198ca --- /dev/null +++ b/test/helpers/utils.js @@ -0,0 +1,110 @@ +'use strict' + +const TestNode = require('./test-node') +const PeerInfo = require('peer-info') +const PeerId = require('peer-id') +const eachAsync = require('async/each') +const pull = require('pull-stream') + +exports.createNodes = function createNodes (configNodes, callback) { + const nodes = {} + eachAsync(Object.keys(configNodes), (key, cb1) => { + let config = configNodes[key] + + const setup = (err, peer) => { + if (err) { + callback(err) + } + + eachAsync(config.addrs, (addr, cb2) => { + peer.multiaddrs.add(addr) + cb2() + }, (err) => { + if (err) { + return callback(err) + } + + nodes[key] = new TestNode(peer, config.transports, config.muxer, config.config) + cb1() + }) + } + + if (config.id) { + PeerId.createFromJSON(config.id, (err, peerId) => { + if (err) return callback(err) + PeerInfo.create(peerId, setup) + }) + } else { + PeerInfo.create(setup) + } + }, (err) => { + if (err) { + return callback(err) + } + + startNodes(nodes, (err) => { + if (err) { + callback(err) + } + + callback(null, nodes) + }) + }) +} + +function startNodes (nodes, callback) { + eachAsync(Object.keys(nodes), + (key, cb) => { + nodes[key].start(cb) + }, + (err) => { + if (err) { + return callback(err) + } + callback(null) + }) +} + +exports.stopNodes = function stopNodes (nodes, callback) { + eachAsync(Object.keys(nodes), + (key, cb) => { + nodes[key].stop(cb) + }, + (err) => { + if (err) { + return callback(err) + } + callback() + }) +} + +function reverse (protocol, conn) { + pull( + conn, + pull.map((data) => { + return data.toString().split('').reverse().join('') + }), + conn + ) +} + +exports.dialAndReverse = function dialAndRevers (srcNode, dstNode, vals, done) { + dstNode.handle('/ipfs/reverse/1.0.0', reverse) + + srcNode.dial(dstNode.peerInfo, '/ipfs/reverse/1.0.0', (err, conn) => { + if (err) return done(err) + + pull( + pull.values(vals), + conn, + pull.collect((err, data) => { + if (err) return done(err) + + let reversed = data.map((val, i) => { + return val.toString() + }) + + srcNode.hangUp(srcNode.peerInfo, () => done(null, reversed)) + })) + }) +} diff --git a/test/listener.spec.js b/test/listener.spec.js new file mode 100644 index 0000000..5056ed4 --- /dev/null +++ b/test/listener.spec.js @@ -0,0 +1,166 @@ +/* eslint-env mocha */ +'use strict' + +const Listener = require('../src/listener') +const nodes = require('./fixtures/nodes') +const Connection = require('interface-connection').Connection +const multicodec = require('../src/multicodec') +const constants = require('../src/circuit/constants') +const handshake = require('pull-handshake') +const waterfall = require('async/waterfall') +const PeerInfo = require('peer-info') +const PeerId = require('peer-id') +const lp = require('pull-length-prefixed') +const pull = require('pull-stream') +const multiaddr = require('multiaddr') +const longaddr = require('./fixtures/long-address') + +const sinon = require('sinon') +const expect = require('chai').expect + +describe('listener', function () { + describe(`listen for relayed connections`, function () { + let listener + + let swarm + let conn + let stream + let shake + let handlerSpy + + beforeEach(function (done) { + stream = handshake({timeout: 1000 * 60}) + shake = stream.handshake + conn = new Connection(stream) + conn.setPeerInfo(new PeerInfo(PeerId.createFromB58String('QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE'))) + + waterfall([ + (cb) => PeerId.createFromJSON(nodes.node4, cb), + (peerId, cb) => PeerInfo.create(peerId, cb), + (peer, cb) => { + peer.multiaddrs.add('/p2p-circuit/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE') + swarm = { + _peerInfo: peer, + handle: sinon.spy((proto, h) => { + handlerSpy = sinon.spy(h) + }), + conns: { + QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE: new Connection() + } + } + + listener = Listener(swarm, {}, () => {}) + listener.listen() + cb() + } + ], done) + }) + + afterEach(() => { + handlerSpy.reset() + }) + + it(`handle a valid request`, function (done) { + pull( + pull.values([Buffer.from(`/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`)]), + lp.encode(), + pull.collect((err, encoded) => { + expect(err).to.be.null + + shake.write(encoded[0]) + lp.decodeFromReader(shake, (err, msg) => { + expect(err).to.be.null + expect(msg.toString()).to.equal(String(constants.RESPONSE.SUCCESS)) + done() + }) + }) + ) + handlerSpy(multicodec.hop, conn) + }) + + it(`handle request with invalid multiaddress`, function (done) { + pull( + pull.values([Buffer.from(`sdfsddfds`)]), + lp.encode(), + pull.collect((err, encoded) => { + expect(err).to.be.null + + shake.write(encoded[0]) + lp.decodeFromReader(shake, (err, msg) => { + expect(err).to.be.null + expect(msg.toString()).to.equal(String(constants.RESPONSE.STOP.SRC_MULTIADDR_INVALID)) + done() + }) + }) + ) + handlerSpy(multicodec.hop, conn) + }) + + it(`handle request with a long multiaddress`, function (done) { + pull( + pull.values([longaddr]), + lp.encode(), + pull.collect((err, encoded) => { + expect(err).to.be.null + + shake.write(encoded[0]) + lp.decodeFromReader(shake, (err, msg) => { + expect(err).to.be.null + expect(msg.toString()).to.equal(String(constants.RESPONSE.STOP.SRC_ADDR_TOO_LONG)) + done() + }) + }) + ) + handlerSpy(multicodec.hop, conn) + }) + }) + + describe(`getAddrs`, function () { + let swarm = null + let listener = null + let peerInfo = null + + beforeEach(function (done) { + waterfall([ + (cb) => PeerId.createFromJSON(nodes.node4, cb), + (peerId, cb) => PeerInfo.create(peerId, cb), + (peer, cb) => { + swarm = { + _peerInfo: peer + } + + peerInfo = peer + listener = Listener(swarm, {}, () => {}) + cb() + } + ], done) + }) + + afterEach(() => { + peerInfo = null + }) + + it(`should return correct addrs`, function () { + peerInfo.multiaddrs.add(`/ip4/0.0.0.0/tcp/4002`) + peerInfo.multiaddrs.add(`/ip4/127.0.0.1/tcp/4003/ws`) + + listener.getAddrs((err, addrs) => { + expect(err).to.be.null + expect(addrs).to.deep.equal([ + multiaddr(`/p2p-circuit/ip4/0.0.0.0/tcp/4002/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`), + multiaddr(`/p2p-circuit/ip4/127.0.0.1/tcp/4003/ws/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`)]) + }) + }) + + it(`don't return default addrs in an explicit p2p-circuit addres`, function () { + peerInfo.multiaddrs.add(`/ip4/127.0.0.1/tcp/4003/ws`) + peerInfo.multiaddrs.add(`/p2p-circuit/ip4/0.0.0.0/tcp/4002`) + listener.getAddrs((err, addrs) => { + expect(err).to.be.null + expect(addrs[0] + .toString()) + .to.equal(`/p2p-circuit/ip4/0.0.0.0/tcp/4002/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`) + }) + }) + }) +}) diff --git a/test/onion-dialer.spec.js b/test/onion-dialer.spec.js new file mode 100644 index 0000000..902277e --- /dev/null +++ b/test/onion-dialer.spec.js @@ -0,0 +1,172 @@ +/* eslint-env mocha */ +'use strict' + +const Dialer = require('../src/circuit/onion-dialer') +const nodes = require('./fixtures/nodes') +const Connection = require('interface-connection').Connection +const multiaddr = require('multiaddr') +const PeerInfo = require('peer-info') +const PeerId = require('peer-id') +const waterfall = require('async/waterfall') + +const sinon = require('sinon') +const expect = require('chai').expect + +describe('onion dialer tests', function () { + describe('dial', function () { + const dialer = sinon.createStubInstance(Dialer) + + beforeEach(function (done) { + waterfall([ + (cb) => PeerId.createFromJSON(nodes.node4, cb), + (peerId, cb) => PeerInfo.create(peerId, cb), + (peer, cb) => { + dialer.swarm = { + _peerInfo: peer + } + cb() + } + ], done) + + dialer.dial.callThrough() + }) + + afterEach(function () { + dialer.dial.reset() + dialer._onionDial.reset() + }) + + it(`split the multiaddr correctly`, function (done) { + dialer._onionDial.callsArgWith(1, null, new Connection()) + + const chainedAddr = multiaddr( + `/ip4/0.0.0.0/tcp/9033/ws/ipfs/${nodes.node2.id}/p2p-circuit` + + `/ip4/0.0.0.0/tcp/9031/ipfs/${nodes.node1.id}/p2p-circuit/`) + + dialer.dial(chainedAddr + `/ipfs/${nodes.node3.id}`, (err, conn) => { + expect(err).to.be.null + expect(dialer._onionDial.calledOnce).to.be.ok + expect(dialer._onionDial.calledWith([ + `/ip4/0.0.0.0/tcp/9033/ws/ipfs/${nodes.node2.id}`, + `/ip4/0.0.0.0/tcp/9031/ipfs/${nodes.node1.id}`, + `/ipfs/${nodes.node3.id}` + ])).to.be.ok + done() + }) + }) + + it(`not dial to itself`, function (done) { + const chainedAddr = multiaddr( + `/ip4/0.0.0.0/tcp/9033/ws/ipfs/${nodes.node2.id}/p2p-circuit` + + `/ip4/0.0.0.0/tcp/9031/ipfs/${nodes.node1.id}/p2p-circuit/`) + + dialer.dial(chainedAddr + `/ipfs/${nodes.node4.id}`, (err, conn) => { + expect(err).to.not.be.null + expect(err).to.be.equal(`cant dial to self!`) + expect(dialer._onionDial.calledOnce).to.not.be.ok + done() + }) + }) + }) + + describe('_onionDial', function () { + describe('_onionDial chained address', function () { + const dialer = sinon.createStubInstance(Dialer) + dialer.relayPeers = new Map() + + let swarm = null + let upgraded = null + beforeEach(function (done) { + waterfall([ + (cb) => PeerId.createFromJSON(nodes.node4, cb), + (peerId, cb) => PeerInfo.create(peerId, cb), + (peer, cb) => { + swarm = { + _peerInfo: peer, + _peerBook: { + get: () => new PeerInfo(PeerId.createFromB58String('QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE')), + has: () => true + } + } + dialer.swarm = swarm + cb() + }, + (cb) => { + upgraded = new Connection() + dialer._onionDial.callThrough() + dialer.dialPeer.onCall(0).callsArgWith(2, null, new Connection()) + dialer._createRelayPipe.onCall(0).callsArgWith(2, null, upgraded) + dialer.dialPeer.onCall(1).callsArgWith(2, null, new Connection()) + cb() + } + ], done) + }) + + afterEach(function () { + dialer.dial.reset() + dialer.dialRelay.reset() + }) + + it(`dial chained multiaddr correctly`, function (done) { + const chainedAddrs = [ + `/ip4/0.0.0.0/tcp/9033/ws/ipfs/${nodes.node2.id}`, + `/ip4/0.0.0.0/tcp/9031/ipfs/${nodes.node1.id}`, + `/ipfs/${nodes.node3.id}` + ] + + const addr1 = multiaddr(chainedAddrs[0]) + const addr2 = multiaddr(chainedAddrs[1]) + const addr3 = multiaddr(chainedAddrs[2]) + + dialer._onionDial(chainedAddrs, (err, conn) => { + expect(err).to.be.null + expect(dialer._createRelayPipe.calledWith(addr2, addr1)).to.be.ok + expect(dialer.dialPeer.calledWith(addr3, upgraded)).to.be.ok + done() + }) + }) + }) + + describe('_onionDial non chained address', function () { + const dialer = sinon.createStubInstance(Dialer) + dialer.relayPeers = new Map() + + let swarm + beforeEach(function (done) { + waterfall([ + (cb) => PeerId.createFromJSON(nodes.node4, cb), + (peerId, cb) => PeerInfo.create(peerId, cb), + (peer, cb) => { + swarm = { + _peerInfo: peer, + _peerBook: { + get: () => new PeerInfo(PeerId.createFromB58String('QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE')), + has: () => true + } + } + dialer.swarm = swarm + cb() + }, + (cb) => { + dialer._onionDial.callThrough() + dialer.dialPeer.onCall(0).callsArgWith(2, null, new Connection()) + cb() + } + ], done) + }) + + afterEach(function () { + dialer.dial.reset() + dialer.dialRelay.reset() + }) + + it(`dial chained multiaddr correctly`, function (done) { + dialer._onionDial([`/ip4/0.0.0.0/tcp/9033/ws/ipfs/${nodes.node2.id}`], (err, conn) => { + expect(err).to.be.null + expect(dialer.dialPeer.calledWith(multiaddr(`/ip4/0.0.0.0/tcp/9033/ws/ipfs/${nodes.node2.id}`))).to.be.ok + done() + }) + }) + }) + }) +}) diff --git a/test/relay.spec.js b/test/relay.spec.js new file mode 100644 index 0000000..9173ca8 --- /dev/null +++ b/test/relay.spec.js @@ -0,0 +1,130 @@ +/* eslint-env mocha */ +'use strict' + +const Relay = require('../src/circuit/relay') +const Dialer = require('../src/circuit/dialer') +const nodes = require('./fixtures/nodes') +const Connection = require('interface-connection').Connection +const multiaddr = require('multiaddr') +const multicodec = require('../src/multicodec') +const constants = require('../src/circuit/constants') +const handshake = require('pull-handshake') +const waterfall = require('async/waterfall') +const PeerInfo = require('peer-info') +const PeerId = require('peer-id') +const longaddr = require('./fixtures/long-address') + +const sinon = require('sinon') +const expect = require('chai').expect + +describe('relay', function () { + describe(`handle circuit requests`, function () { + const relay = sinon.createStubInstance(Relay) + const dialer = sinon.createStubInstance(Dialer) + + let swarm + let fromConn + let toConn + let stream + let shake + let handlerSpy + + beforeEach(function (done) { + stream = handshake({timeout: 1000 * 60}) + shake = stream.handshake + fromConn = new Connection(stream) + fromConn.setPeerInfo(new PeerInfo(PeerId.createFromB58String('QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE'))) + toConn = new Connection(shake.rest()) + + waterfall([ + (cb) => PeerId.createFromJSON(nodes.node4, cb), + (peerId, cb) => PeerInfo.create(peerId, cb), + (peer, cb) => { + peer.multiaddrs.add('/p2p-circuit/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE') + swarm = { + _peerInfo: peer, + handle: sinon.spy((proto, h) => { + handlerSpy = sinon.spy(h) + }), + conns: { + QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE: new Connection() + } + } + + dialer.swarm = swarm + cb() + } + ], () => { + relay.mount.callThrough() + relay.emit.callThrough() + relay.on.callThrough() + relay._readDstAddr.callThrough() + relay._readSrcAddr.callThrough() + relay._writeErr.callThrough() + relay.mount(swarm) // mount the swarm + relay._circuit.callsArg(3, null, toConn) + + dialer.relayConns = new Map() + dialer.negotiateRelay.callThrough() + done() + }) + }) + + afterEach(() => { + relay.mount.reset() + relay.emit.reset() + relay.on.reset() + relay._circuit.reset() + relay._readDstAddr.reset() + relay._readSrcAddr.reset() + relay._readSrcAddr.reset() + dialer.negotiateRelay.reset() + }) + + it(`handle a valid circuit request`, function (done) { + relay.active = true + relay.on('circuit:success', () => { + expect(relay._circuit.calledWith(sinon.match.any, dstMa)).to.be.ok + done() + }) + + let dstMa = multiaddr(`/ip4/0.0.0.0/tcp/9033/ws/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`) + dialer.negotiateRelay(fromConn, dstMa, () => {}) + handlerSpy(multicodec.hop, toConn) + }) + + // it(`fail dialing to invalid multiaddr`, function () { + // // TODO: implement without relying on negotiateRelay + // }) + + it(`not dial to self`, function (done) { + let dstMa = multiaddr(`/ipfs/${nodes.node4.id}`) + dialer.negotiateRelay(fromConn, dstMa, (err, newConn) => { + expect(err).to.not.be.null + expect(err).to.be.an.instanceOf(Error) + expect(err.message) + .to + .equal(`Got ${constants.RESPONSE.HOP.CANT_CONNECT_TO_SELF} error code trying to dial over relay`) + expect(newConn).to.be.undefined + done() + }) + + handlerSpy(multicodec.hop, toConn) + }) + + it(`fail on address exceeding 1024 bytes`, function (done) { + let dstMa = multiaddr(longaddr.toString()) + dialer.negotiateRelay(fromConn, dstMa, (err, newConn) => { + expect(err).to.not.be.null + expect(err).to.be.an.instanceOf(Error) + expect(err.message) + .to + .equal(`Got ${constants.RESPONSE.HOP.DST_ADDR_TOO_LONG} error code trying to dial over relay`) + expect(newConn).to.be.undefined + done() + }) + + handlerSpy(multicodec.hop, toConn) + }) + }) +}) From 4f33101dec5e76babe369c1e5c8d8cca21435959 Mon Sep 17 00:00:00 2001 From: Dmitriy Ryajov Date: Wed, 10 May 2017 17:05:48 -0700 Subject: [PATCH 03/20] refactor: renaming Relay to Hop --- src/circuit/{relay.js => hop.js} | 4 ++-- test/{relay.spec.js => hop.spec.js} | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename src/circuit/{relay.js => hop.js} (99%) rename test/{relay.spec.js => hop.spec.js} (97%) diff --git a/src/circuit/relay.js b/src/circuit/hop.js similarity index 99% rename from src/circuit/relay.js rename to src/circuit/hop.js index 650c3e9..c90c308 100644 --- a/src/circuit/relay.js +++ b/src/circuit/hop.js @@ -21,7 +21,7 @@ const log = debug('libp2p:swarm:circuit:relay') log.err = debug('libp2p:swarm:circuit:error:relay') let utils -class Relay extends EE { +class Hop extends EE { /** * Construct a Circuit object * @@ -197,4 +197,4 @@ class Relay extends EE { } } -module.exports = Relay +module.exports = Hop diff --git a/test/relay.spec.js b/test/hop.spec.js similarity index 97% rename from test/relay.spec.js rename to test/hop.spec.js index 9173ca8..5ce171e 100644 --- a/test/relay.spec.js +++ b/test/hop.spec.js @@ -1,7 +1,7 @@ /* eslint-env mocha */ 'use strict' -const Relay = require('../src/circuit/relay') +const Hop = require('../src/circuit/hop') const Dialer = require('../src/circuit/dialer') const nodes = require('./fixtures/nodes') const Connection = require('interface-connection').Connection @@ -19,7 +19,7 @@ const expect = require('chai').expect describe('relay', function () { describe(`handle circuit requests`, function () { - const relay = sinon.createStubInstance(Relay) + const relay = sinon.createStubInstance(Hop) const dialer = sinon.createStubInstance(Dialer) let swarm From f195e123767b7876f6efcb43df36676c4d95f890 Mon Sep 17 00:00:00 2001 From: Dmitriy Ryajov Date: Wed, 10 May 2017 17:06:56 -0700 Subject: [PATCH 04/20] chore: fixing license --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index bbfffbf..f94290a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017 libp2p +Copyright (c) 2017 Protocol Labs Inc Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 732ccda640d251180914584f3342c3d12faffcd4 Mon Sep 17 00:00:00 2001 From: Dmitriy Ryajov Date: Wed, 10 May 2017 17:07:39 -0700 Subject: [PATCH 05/20] chore: removing custom eslint config --- package.json | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 0359395..71be26a 100644 --- a/package.json +++ b/package.json @@ -30,21 +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" - ], - "rules": { - "strict": "off" - } - }, "devDependencies": { "aegir": "^10.0.0", "chai": "^3.5.0", - "multihashes": "^0.4.5", "pre-commit": "^1.2.2", "proxyquire": "^1.7.11", - "pull-pushable": "^2.0.1", "sinon": "^2.1.0" }, "contributors": [], @@ -53,13 +43,14 @@ "debug": "^2.6.1", "interface-connection": "^0.3.1", "lodash": "^4.17.4", + "mafmt": "^2.1.8", "multiaddr": "^2.2.1", "multistream-select": "^0.13.4", "peer-id": "^0.8.2", - "peer-info": "^0.8.3", + "peer-info": "^0.8.5", "pull-abortable": "^4.1.0", "pull-handshake": "^1.1.4", - "pull-stream": "^3.5.0", + "pull-stream": "^3.5.1", "safe-buffer": "^5.0.1", "setimmediate": "^1.0.5" } From 2f977a9c43354feba6893f89c072829ed7e187ba Mon Sep 17 00:00:00 2001 From: Dmitriy Ryajov Date: Wed, 10 May 2017 17:08:22 -0700 Subject: [PATCH 06/20] refactor: extract listening functionality into Stop handler --- src/circuit/stop.js | 83 +++++++++++++++++++++++++++++++++ src/index.js | 2 +- src/listener.js | 61 ++---------------------- test/listener.spec.js | 105 ------------------------------------------ test/stop.spec.js | 105 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 192 insertions(+), 164 deletions(-) create mode 100644 src/circuit/stop.js create mode 100644 test/stop.spec.js diff --git a/src/circuit/stop.js b/src/circuit/stop.js new file mode 100644 index 0000000..f91969d --- /dev/null +++ b/src/circuit/stop.js @@ -0,0 +1,83 @@ +'use strict' + +require('setimmediate') + +const EE = require('events').EventEmitter +const Buffer = require('safe-buffer').Buffer +const waterfall = require('async/waterfall') +const StreamHandler = require('./stream-handler') +const constants = require('./constants') +const multiaddr = require('multiaddr') +const Connection = require('interface-connection').Connection + +const debug = require('debug') + +const log = debug('libp2p:circuit:stop') +log.err = debug('libp2p:circuit:error:stop') + +class Stop extends EE { + constructor (swarm) { + super() + this.swarm = swarm + } + + handle (conn, callback) { + callback = callback || (() => {}) + + conn.getPeerInfo((err, peerInfo) => { + if (err) { + log.err('Failed to identify incoming connection', err) + return callback(err, null) + } + + const streamHandler = new StreamHandler(conn) + waterfall([ + (cb) => { + streamHandler.read((err, msg) => { + if (err) { + log.err(err) + + if (err.includes('size longer than max permitted length of')) { + const errCode = String(constants.RESPONSE.STOP.SRC_ADDR_TOO_LONG) + setImmediate(() => this.emit('circuit:error', errCode)) + streamHandler.write([Buffer.from(errCode)]) + } + + return cb(err) + } + + let srcMa = null + try { + srcMa = multiaddr(msg.toString()) + } catch (err) { + const errCode = String(constants.RESPONSE.STOP.SRC_MULTIADDR_INVALID) + setImmediate(() => this.emit('circuit:error', errCode)) + streamHandler.write([Buffer.from(errCode)]) + return cb(errCode) + } + + // add the addr we got along with the relay request + peerInfo.multiaddrs.add(srcMa) + cb() + }) + }, + (cb) => { + streamHandler.write([Buffer.from(String(constants.RESPONSE.SUCCESS))], (err) => { + if (err) { + log.err(err) + return cb(err) + } + + const newConn = new Connection(streamHandler.rest(), conn) + newConn.setPeerInfo(peerInfo) + setImmediate(() => this.emit('connection', newConn)) + callback(newConn) + cb() + }) + } + ]) + }) + } +} + +module.exports = Stop diff --git a/src/index.js b/src/index.js index 92a3667..1c2002c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,7 @@ 'use strict' module.exports = { - Relay: require('./circuit/relay'), + Stop: require('./circuit/hop'), Dialer: require('./dialer'), multicodec: require('./multicodec'), tag: 'Circuit' diff --git a/src/listener.js b/src/listener.js index 7017110..6333959 100644 --- a/src/listener.js +++ b/src/listener.js @@ -1,16 +1,12 @@ 'use strict' -require('safe-buffer') require('setimmediate') const multicodec = require('./multicodec') const EE = require('events').EventEmitter const multiaddr = require('multiaddr') -const Connection = require('interface-connection').Connection const mafmt = require('mafmt') -const constants = require('./circuit/constants') -const waterfall = require('async/waterfall') -const StreamHandler = require('./circuit/stream-handler') +const Stop = require('./circuit/stop') const debug = require('debug') @@ -19,64 +15,13 @@ log.err = debug('libp2p:circuit:error:listener') module.exports = (swarm, options, handler) => { const listener = new EE() + const stopHandler = new Stop(swarm) listener.listen = (ma, callback) => { callback = callback || (() => {}) swarm.handle(multicodec.stop, (proto, conn) => { - conn.getPeerInfo((err, peerInfo) => { - if (err) { - log.err('Failed to identify incoming connection', err) - return handler(err, null) - } - - const streamHandler = new StreamHandler(conn) - waterfall([ - (cb) => { - streamHandler.read((err, msg) => { - if (err) { - log.err(err) - - if (err.includes('size longer than max permitted length of')) { - const errCode = String(constants.RESPONSE.STOP.SRC_ADDR_TOO_LONG) - setImmediate(() => this.emit('circuit:error', errCode)) - streamHandler.write([Buffer.from(errCode)]) - } - - return cb(err) - } - - let srcMa = null - try { - srcMa = multiaddr(msg.toString()) - } catch (err) { - const errCode = String(constants.RESPONSE.STOP.SRC_MULTIADDR_INVALID) - setImmediate(() => this.emit('circuit:error', errCode)) - streamHandler.write([Buffer.from(errCode)]) - return cb(errCode) - } - - // add the addr we got along with the relay request - peerInfo.multiaddrs.add(srcMa) - cb() - }) - }, - (cb) => { - streamHandler.write([Buffer.from(String(constants.RESPONSE.SUCCESS))], (err) => { - if (err) { - log.err(err) - return cb(err) - } - - const newConn = new Connection(streamHandler.rest(), conn) - newConn.setPeerInfo(peerInfo) - setImmediate(() => listener.emit('connection', newConn)) - handler(newConn) - cb() - }) - } - ]) - }) + stopHandler.handle(conn, handler) }) setImmediate(() => listener.emit('listen')) diff --git a/test/listener.spec.js b/test/listener.spec.js index 5056ed4..54071d9 100644 --- a/test/listener.spec.js +++ b/test/listener.spec.js @@ -3,118 +3,13 @@ const Listener = require('../src/listener') const nodes = require('./fixtures/nodes') -const Connection = require('interface-connection').Connection -const multicodec = require('../src/multicodec') -const constants = require('../src/circuit/constants') -const handshake = require('pull-handshake') const waterfall = require('async/waterfall') const PeerInfo = require('peer-info') const PeerId = require('peer-id') -const lp = require('pull-length-prefixed') -const pull = require('pull-stream') const multiaddr = require('multiaddr') -const longaddr = require('./fixtures/long-address') - -const sinon = require('sinon') const expect = require('chai').expect describe('listener', function () { - describe(`listen for relayed connections`, function () { - let listener - - let swarm - let conn - let stream - let shake - let handlerSpy - - beforeEach(function (done) { - stream = handshake({timeout: 1000 * 60}) - shake = stream.handshake - conn = new Connection(stream) - conn.setPeerInfo(new PeerInfo(PeerId.createFromB58String('QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE'))) - - waterfall([ - (cb) => PeerId.createFromJSON(nodes.node4, cb), - (peerId, cb) => PeerInfo.create(peerId, cb), - (peer, cb) => { - peer.multiaddrs.add('/p2p-circuit/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE') - swarm = { - _peerInfo: peer, - handle: sinon.spy((proto, h) => { - handlerSpy = sinon.spy(h) - }), - conns: { - QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE: new Connection() - } - } - - listener = Listener(swarm, {}, () => {}) - listener.listen() - cb() - } - ], done) - }) - - afterEach(() => { - handlerSpy.reset() - }) - - it(`handle a valid request`, function (done) { - pull( - pull.values([Buffer.from(`/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`)]), - lp.encode(), - pull.collect((err, encoded) => { - expect(err).to.be.null - - shake.write(encoded[0]) - lp.decodeFromReader(shake, (err, msg) => { - expect(err).to.be.null - expect(msg.toString()).to.equal(String(constants.RESPONSE.SUCCESS)) - done() - }) - }) - ) - handlerSpy(multicodec.hop, conn) - }) - - it(`handle request with invalid multiaddress`, function (done) { - pull( - pull.values([Buffer.from(`sdfsddfds`)]), - lp.encode(), - pull.collect((err, encoded) => { - expect(err).to.be.null - - shake.write(encoded[0]) - lp.decodeFromReader(shake, (err, msg) => { - expect(err).to.be.null - expect(msg.toString()).to.equal(String(constants.RESPONSE.STOP.SRC_MULTIADDR_INVALID)) - done() - }) - }) - ) - handlerSpy(multicodec.hop, conn) - }) - - it(`handle request with a long multiaddress`, function (done) { - pull( - pull.values([longaddr]), - lp.encode(), - pull.collect((err, encoded) => { - expect(err).to.be.null - - shake.write(encoded[0]) - lp.decodeFromReader(shake, (err, msg) => { - expect(err).to.be.null - expect(msg.toString()).to.equal(String(constants.RESPONSE.STOP.SRC_ADDR_TOO_LONG)) - done() - }) - }) - ) - handlerSpy(multicodec.hop, conn) - }) - }) - describe(`getAddrs`, function () { let swarm = null let listener = null diff --git a/test/stop.spec.js b/test/stop.spec.js new file mode 100644 index 0000000..3685afe --- /dev/null +++ b/test/stop.spec.js @@ -0,0 +1,105 @@ +/* eslint-env mocha */ +'use strict' + +const Stop = require('../src/circuit/stop') +const nodes = require('./fixtures/nodes') +const Connection = require('interface-connection').Connection +const constants = require('../src/circuit/constants') +const handshake = require('pull-handshake') +const waterfall = require('async/waterfall') +const PeerInfo = require('peer-info') +const PeerId = require('peer-id') +const lp = require('pull-length-prefixed') +const pull = require('pull-stream') +const longaddr = require('./fixtures/long-address') + +const expect = require('chai').expect + +describe('stop', function () { + describe(`handle relayed connections`, function () { + let stopHandler + + let swarm + let conn + let stream + let shake + + beforeEach(function (done) { + stream = handshake({timeout: 1000 * 60}) + shake = stream.handshake + conn = new Connection(stream) + conn.setPeerInfo(new PeerInfo(PeerId.createFromB58String('QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE'))) + + waterfall([ + (cb) => PeerId.createFromJSON(nodes.node4, cb), + (peerId, cb) => PeerInfo.create(peerId, cb), + (peer, cb) => { + peer.multiaddrs.add('/p2p-circuit/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE') + swarm = { + _peerInfo: peer, + conns: { + QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE: new Connection() + } + } + + stopHandler = new Stop(swarm) + cb() + } + ], done) + }) + + it(`handle a valid request`, function (done) { + pull( + pull.values([Buffer.from(`/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`)]), + lp.encode(), + pull.collect((err, encoded) => { + expect(err).to.be.null + + shake.write(encoded[0]) + lp.decodeFromReader(shake, (err, msg) => { + expect(err).to.be.null + expect(msg.toString()).to.equal(String(constants.RESPONSE.SUCCESS)) + done() + }) + }) + ) + stopHandler.handle(conn) + }) + + it(`handle request with invalid multiaddress`, function (done) { + pull( + pull.values([Buffer.from(`sdfsddfds`)]), + lp.encode(), + pull.collect((err, encoded) => { + expect(err).to.be.null + + shake.write(encoded[0]) + lp.decodeFromReader(shake, (err, msg) => { + expect(err).to.be.null + expect(msg.toString()).to.equal(String(constants.RESPONSE.STOP.SRC_MULTIADDR_INVALID)) + done() + }) + }) + ) + stopHandler.handle(conn) + }) + + it(`handle request with a long multiaddress`, function (done) { + pull( + pull.values([longaddr]), + lp.encode(), + pull.collect((err, encoded) => { + expect(err).to.be.null + + shake.write(encoded[0]) + lp.decodeFromReader(shake, (err, msg) => { + expect(err).to.be.null + expect(msg.toString()).to.equal(String(constants.RESPONSE.STOP.SRC_ADDR_TOO_LONG)) + done() + }) + }) + ) + stopHandler.handle(conn) + }) + }) +}) From 011e7bebd3a13b606045e9041593f16aa3d062ab Mon Sep 17 00:00:00 2001 From: Dmitriy Ryajov Date: Wed, 10 May 2017 17:13:34 -0700 Subject: [PATCH 07/20] fix: removing unused priority functionality --- src/circuit/constants.js | 1 - src/dialer.js | 8 -------- 2 files changed, 9 deletions(-) diff --git a/src/circuit/constants.js b/src/circuit/constants.js index 876b479..1abd505 100644 --- a/src/circuit/constants.js +++ b/src/circuit/constants.js @@ -1,7 +1,6 @@ 'use strict' module.exports = { - PRIOIRY: 100, DIALER: { ONION: 'onion', TELESCOPE: 'telescope' diff --git a/src/dialer.js b/src/dialer.js index d44ba7a..34608b4 100644 --- a/src/dialer.js +++ b/src/dialer.js @@ -72,14 +72,6 @@ class Dialer { }) } - get priority () { - return constants.PRIOIRY // TODO: move to a constants file that all transports can share - } - - set priority (val) { - throw new Error('Priority is read only!') - } - /** * Dial a peer over a relay * From a5af86823fc938b1f46c632bb79f96ca6257466b Mon Sep 17 00:00:00 2001 From: Dmitriy Ryajov Date: Wed, 10 May 2017 17:14:05 -0700 Subject: [PATCH 08/20] fix: correct import of safe-buffer --- src/circuit/dialer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/circuit/dialer.js b/src/circuit/dialer.js index b1fe822..0cbfb21 100644 --- a/src/circuit/dialer.js +++ b/src/circuit/dialer.js @@ -1,6 +1,6 @@ 'use strict' -require('safe-buffer') +const Buffer = require('safe-buffer').Buffer const Connection = require('interface-connection').Connection const isFunction = require('lodash.isfunction') From 1dcab27b0315be7ab97b395462de9acff8babeed Mon Sep 17 00:00:00 2001 From: Dmitriy Ryajov Date: Wed, 10 May 2017 17:14:31 -0700 Subject: [PATCH 09/20] lint: remove unused import --- src/dialer.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/dialer.js b/src/dialer.js index 34608b4..51fc6be 100644 --- a/src/dialer.js +++ b/src/dialer.js @@ -3,7 +3,6 @@ const mafmt = require('mafmt') const multiaddr = require('multiaddr') -const constants = require('./circuit/constants') const OnionDialer = require('./circuit/onion-dialer') const utilsFactory = require('./circuit/utils') From f199867151e011f77b031076820b02a394e71644 Mon Sep 17 00:00:00 2001 From: Dmitriy Ryajov Date: Wed, 10 May 2017 17:23:47 -0700 Subject: [PATCH 10/20] refactor: use async/setImmediate --- src/circuit/onion-dialer.js | 3 +-- src/listener.js | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/circuit/onion-dialer.js b/src/circuit/onion-dialer.js index 3961ba1..05667a8 100644 --- a/src/circuit/onion-dialer.js +++ b/src/circuit/onion-dialer.js @@ -1,7 +1,6 @@ 'use strict' -require('setimmediate') -require('safe-buffer') +const setImmediate = require('async/setImmediate') const Dialer = require('./dialer') const isFunction = require('lodash.isfunction') diff --git a/src/listener.js b/src/listener.js index 6333959..7dfe0ed 100644 --- a/src/listener.js +++ b/src/listener.js @@ -1,6 +1,6 @@ 'use strict' -require('setimmediate') +const setImmediate = require('async/setImmediate') const multicodec = require('./multicodec') const EE = require('events').EventEmitter From 7f51231d08b53ad34679682b7dfcc13c17997ae5 Mon Sep 17 00:00:00 2001 From: Dmitriy Ryajov Date: Wed, 10 May 2017 20:30:21 -0700 Subject: [PATCH 11/20] fix: remove unused constant --- src/circuit/constants.js | 1 - test/dialer.spec.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/circuit/constants.js b/src/circuit/constants.js index 1abd505..8799d2f 100644 --- a/src/circuit/constants.js +++ b/src/circuit/constants.js @@ -7,7 +7,6 @@ module.exports = { }, RESPONSE: { SUCCESS: 100, - FAILURE: 500, HOP: { SRC_ADDR_TOO_LONG: 220, DST_ADDR_TOO_LONG: 221, diff --git a/test/dialer.spec.js b/test/dialer.spec.js index c55d7c2..17efa13 100644 --- a/test/dialer.spec.js +++ b/test/dialer.spec.js @@ -125,7 +125,7 @@ describe('dialer tests', function () { if (err) return done(err) pull( - pull.values([Buffer.from(String(constants.RESPONSE.FAILURE))]), + pull.values([Buffer.from(String(500))]), // send arbitrary non 200 code lp.encode(), pull.collect((err, encoded) => { expect(err).to.be.null From 70fcc87dca02be7290174c5685b3d295ef04019f Mon Sep 17 00:00:00 2001 From: Dmitriy Ryajov Date: Wed, 10 May 2017 21:14:44 -0700 Subject: [PATCH 12/20] refactor: move active/passive check out of _readDstAddr --- src/circuit/hop.js | 16 +++++++++++----- src/circuit/stop.js | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/circuit/hop.js b/src/circuit/hop.js index c90c308..dc77785 100644 --- a/src/circuit/hop.js +++ b/src/circuit/hop.js @@ -87,10 +87,6 @@ class Hop extends EE { return this._writeErr(streamHandler, constants.RESPONSE.HOP.CANT_CONNECT_TO_SELF, cb) } - if (!this.active && !utils.isPeerConnected(dstMa.getPeerId())) { - return this._writeErr(streamHandler, constants.RESPONSE.HOP.NO_CONN_TO_DST, cb) - } - cb(null, dstMa) }) } @@ -107,7 +103,17 @@ class Hop extends EE { this.swarm.handle(multicodec.hop, (proto, conn) => { const streamHandler = new StreamHandler(conn, 1000 * 60) waterfall([ - (cb) => this._readDstAddr(streamHandler, cb), + (cb) => this._readDstAddr(streamHandler, (err, dstMa) => { + if (err) { + return cb(err) + } + + if (!this.active && !utils.isPeerConnected(dstMa.getPeerId())) { + return this._writeErr(streamHandler, constants.RESPONSE.HOP.NO_CONN_TO_DST, cb) + } + + cb(null, dstMa) + }), (dstMa, cb) => { this._readSrcAddr(streamHandler, (err, srcMa) => { cb(err, dstMa, srcMa) diff --git a/src/circuit/stop.js b/src/circuit/stop.js index f91969d..291e99d 100644 --- a/src/circuit/stop.js +++ b/src/circuit/stop.js @@ -1,6 +1,6 @@ 'use strict' -require('setimmediate') +const setImmediate = require('async/setImmediate') const EE = require('events').EventEmitter const Buffer = require('safe-buffer').Buffer From 1d4a4cee45eb4684e7532f341d560e9efa148fee Mon Sep 17 00:00:00 2001 From: Dmitriy Ryajov Date: Sun, 4 Jun 2017 14:52:56 -0700 Subject: [PATCH 13/20] feat: initial readme write up --- README.md | 185 +++++++++++++++++++++++++++++++++++++++++---- src/circuit/hop.js | 16 +++- src/index.js | 2 +- test/hop.spec.js | 17 +---- 4 files changed, 191 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 17244b8..9630190 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,197 @@ -# +# 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) +[![Build Status](https://travis-ci.org/libp2p/js-libp2p-circuit.svg?style=flat-square)](https://travis-ci.org/libp2p/js-libp2p-circuit) +[![Coverage Status](https://coveralls.io/repos/github/libp2p/js-libp2p-circuit/badge.svg?branch=master)](https://coveralls.io/github/libp2p/js-libp2p-circuit?branch=master) +[![Dependency Status](https://david-dm.org/libp2p/js-libp2p-circuit.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-circuit) +[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard) +![](https://img.shields.io/badge/npm-%3E%3D3.0.0-orange.svg?style=flat-square) +![](https://img.shields.io/badge/Node.js-%3E%3D4.0.0-orange.svg?style=flat-square) -> +![](https://raw.githubusercontent.com/libp2p/interface-connection/master/img/badge.png) +![](https://raw.githubusercontent.com/libp2p/interface-transport/master/img/badge.png) - +> Node.js implementation of the Circuit module that libp2p uses, which implements the [interface-connection](https://github.com/libp2p/interface-connection) interface for dial/listen. + +`libp2p-circuit` implements the circuit-relay mechanism that allows nodes that don't speak the same protocol to communicate using a third _relay_ node. + +**Note:** This module uses [pull-streams](https://pull-stream.github.io) for all stream based interfaces. + +### Why? + +`circuit-relaying` uses additional nodes in order to transfer traffic between two otherwise unreachable nodes. This allows nodes that don't speak the same protocols or are running in limited environments, e.g. browsers and IoT devices, to communicate, which would otherwise be impossible given the fact that for example browsers don't have any socket support and as such cannot be directly dialed. + +The applicability of circuit-relaying is not limited to routing traffic between browser nodes, other uses include: + - routing traffic between private nets + - circumventing NAT layers + - route mangling for better privacy (matreshka/shallot dialing). + + It's also possible to use it for clients that implement exotic transports such as devices that only have bluetooth radios to be reachable over bluethoot enabled relays and become full p2p nodes. + +### libp2p-circuit and IPFS + +Prior to `libp2p-circuit` there was a rift in the IPFS network, were IPFS nodes could only access content from nodes that speak the same protocol, for example TCP only nodes could only dial to other TCP only nodes, same for any other protocol combination. In practice, this limitation was most visible in JS-IPFS browser nodes, since they can only dial out but not be dialed in over WebRTC or WebSockets, hence any content that the browser node held was not reachable by the rest of the network even through it was announced on the DHT. Non browser IPFS nodes would usually speak more than one protocol such as TCP, WebSockets and/or WebRTC, this made the problem less severe outside of the browser. `libp2p-circuit` solves this problem completely, as long as there are `relay nodes` capable of routing traffic between those nodes their content should be available to the rest of the IPFS network. ## Table of Contents - +- [Install](#install) + - [npm](#npm) +- [Usage](#usage) + - [Example](#example) + - [This module uses `pull-streams`](#this-module-uses-pull-streams) + - [Converting `pull-streams` to Node.js Streams](#converting-pull-streams-to-nodejs-streams) +- [API](#api) +- [Contribute](#contribute) +- [License](#license) ## Install - +### npm + +```sh +> npm i libp2p-circuit +``` ## Usage - +### Example + +#### Create dialer/listener + +```js +const Circuit = require('libp2p-circuit') +const multiaddr = require('multiaddr') +const pull = require('pull-stream') + +const mh1 = multiaddr('/p2p-circuit/ipfs/QmHash') // dial /ipfs/QmHash over any circuit + +const circuit = new Circuit(swarmInstance, options) // pass swarm instance and options + +const listener = circuit.createListener(mh1, (connection) => { + console.log('new connection opened') + pull( + pull.values(['hello']), + socket + ) +}) + +listener.listen(() => { + console.log('listening') + + pull( + circuit.dial(mh1), + pull.log, + pull.onEnd(() => { + circuit.close() + }) + ) +}) +``` + +Outputs: + +```sh +listening +new connection opened +hello +``` + +#### Create `relay` + +```js +const Relay = require('libp2p-circuit').Realy -## Lead +const relay = new Relay(options) + +relay.mount(swarmInstance) // start relaying traffic +``` + +### This module uses `pull-streams` + +We expose a streaming interface based on `pull-streams`, rather then on the Node.js core streams implementation (aka Node.js streams). `pull-streams` offers us a better mechanism for error handling and flow control guarantees. If you would like to know more about why we did this, see the discussion at this [issue](https://github.com/ipfs/js-ipfs/issues/362). + +You can learn more about pull-streams at: + +- [The history of Node.js streams, nodebp April 2014](https://www.youtube.com/watch?v=g5ewQEuXjsQ) +- [The history of streams, 2016](http://dominictarr.com/post/145135293917/history-of-streams) +- [pull-streams, the simple streaming primitive](http://dominictarr.com/post/149248845122/pull-streams-pull-streams-are-a-very-simple) +- [pull-streams documentation](https://pull-stream.github.io/) + +#### Converting `pull-streams` to Node.js Streams + +If you are a Node.js streams user, you can convert a pull-stream to a Node.js stream using the module [`pull-stream-to-stream`](https://github.com/dominictarr/pull-stream-to-stream), giving you an instance of a Node.js stream that is linked to the pull-stream. For example: + +```js +const pullToStream = require('pull-stream-to-stream') + +const nodeStreamInstance = pullToStream(pullStreamInstance) +// nodeStreamInstance is an instance of a Node.js Stream +``` + +To learn more about this utility, visit https://pull-stream.github.io/#pull-stream-to-stream. + +## API + +[![](https://raw.githubusercontent.com/libp2p/interface-transport/master/img/badge.png)](https://github.com/libp2p/interface-transport) + +`libp2p-circuit` accepts Circuit addresses for both IPFS and non IPFS encapsulated addresses, i.e: + +`/p2p-circuit/ip4/127.0.0.1/tcp/4001/ipfs/QmHash` + +Both for dialing and listening. + +### Implementation rational + +This module is not a transport, however it implements `interface-transport` interface in order to allow circuit to be plugged with `libp2p-swarm`. The rational behind it is that, `libp2p-circuit` has a dial and listen flow, which fits nicely with other transports, moreover, it requires the _raw_ connection to be encrypted and muxed just as a regular transport's connection does. All in all, `interface-transport` ended up being the correct level of abstraction for circuit, as well as allowed us to reuse existing integration points in `libp2p-swarm` and `libp2p` without adding any ad-hoc logic. All parts of `interface-transport` are used, including `.getAddr` which returns a list of `/p2p-circuit` addresses that circuit is currently listening. + +``` + + libp2p libp2p-circuit ++-------------------------------------------------+ +---------------------------------------------------------+ +| | | | +| +---------------------------------+ | | | +| | | | | +------------------------+ | +| | | | circuit-relay registers the /hop | | | | +| | libp2p-swarm |<----------------------------------------------------->| circuit-relay | | +| | | | multistream handler with the swarm | | | | +| | | | to handle incomming dial requests | +------------------------+ | +| +---------------------------------+ | from other nodes | transport | +| ^ ^ ^ ^ ^ ^ | | +-------------------------------------------+ | +| | | | | | | | | | +----------------------------------+ | | +| | | | | | | | dialer uses the swarm to dial to a | | | | | | +| | | | +------------------------------------------------------------------------------>| dialer | | | +| | |transports | | relay node listening on the /hop | | | | | | +| | | | | | | multistream endpoint | | +----------------------------------+ | | +| | | | | | | | | | | +| v v | v v | | | | | +|+------------------|----------------------------+| | | +----------------------------------+ | | +|| | | | | || | | | | | | +||libp2p-tcp |libp2p-ws | .... |libp2p-circuit ||listener registers a /stop multistream | | listener | | | +|| | +-------------------------------------------------------------------------------->| | | | +|| | | |pluggs in just ||handler with the swarm to handle | | +----------------------------------+ | | +|| | | |as any other ||incomming relay connections | | | | +|| | | |transport || | +-------------------------------------------+ | +|+-----------------------------------------------+| | | +| | | | +| | | | ++-------------------------------------------------+ +---------------------------------------------------------+ +``` + -- [](https://github.com/) ## Contribute -Please contribute! [Look at the issues](https://github.com/libp2p//issues)! +Contributions are welcome! The libp2p implementation in JavaScript is a work in progress. As such, there's a few things you can do right now to help out: + +- [Check out the existing issues](//github.com/libp2p/js-libp2p-circuit/issues). +- **Perform code reviews**. +- **Add tests**. There can never be enough tests. -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). +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 +[MIT](LICENSE) © 2015-2016 Protocol Labs diff --git a/src/circuit/hop.js b/src/circuit/hop.js index dc77785..1dfac0e 100644 --- a/src/circuit/hop.js +++ b/src/circuit/hop.js @@ -14,6 +14,7 @@ const once = require('once') const utilsFactory = require('./utils') const StreamHandler = require('./stream-handler') const waterfall = require('async/waterfall') +const assignInWith = require('lodash/assignInWith') const multicodec = require('./../multicodec') @@ -33,7 +34,16 @@ class Hop extends EE { */ constructor (options) { super() - this.config = Object.assign({active: false}, options) + this.config = assignInWith( + { + active: false, + enabled: false + }, + options, + (orig, src) => { + typeof src === 'undefined' ? false : src + }) + this.swarm = null this.active = this.config.active } @@ -98,6 +108,10 @@ class Hop extends EE { * @return {void} */ mount (swarm) { + if (!this.config.enabled) { + return + } + this.swarm = swarm utils = utilsFactory(swarm) this.swarm.handle(multicodec.hop, (proto, conn) => { diff --git a/src/index.js b/src/index.js index 1c2002c..4f6f02b 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,7 @@ 'use strict' module.exports = { - Stop: require('./circuit/hop'), + Hop: require('./circuit/hop'), Dialer: require('./dialer'), multicodec: require('./multicodec'), tag: 'Circuit' diff --git a/test/hop.spec.js b/test/hop.spec.js index 5ce171e..e0322c6 100644 --- a/test/hop.spec.js +++ b/test/hop.spec.js @@ -19,9 +19,9 @@ const expect = require('chai').expect describe('relay', function () { describe(`handle circuit requests`, function () { - const relay = sinon.createStubInstance(Hop) const dialer = sinon.createStubInstance(Dialer) + let relay let swarm let fromConn let toConn @@ -55,13 +55,9 @@ describe('relay', function () { cb() } ], () => { - relay.mount.callThrough() - relay.emit.callThrough() - relay.on.callThrough() - relay._readDstAddr.callThrough() - relay._readSrcAddr.callThrough() - relay._writeErr.callThrough() + relay = new Hop({enabled: true}) relay.mount(swarm) // mount the swarm + relay._circuit = sinon.stub() relay._circuit.callsArg(3, null, toConn) dialer.relayConns = new Map() @@ -71,14 +67,7 @@ describe('relay', function () { }) afterEach(() => { - relay.mount.reset() - relay.emit.reset() - relay.on.reset() relay._circuit.reset() - relay._readDstAddr.reset() - relay._readSrcAddr.reset() - relay._readSrcAddr.reset() - dialer.negotiateRelay.reset() }) it(`handle a valid circuit request`, function (done) { From 11222e46755ee7c74ec19584a55b6c96986d5806 Mon Sep 17 00:00:00 2001 From: Dmitriy Ryajov Date: Sun, 2 Jul 2017 14:25:46 -0700 Subject: [PATCH 14/20] fix: output correct circuit addresses --- src/listener.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/listener.js b/src/listener.js index 7dfe0ed..64a8b5f 100644 --- a/src/listener.js +++ b/src/listener.js @@ -57,8 +57,12 @@ module.exports = (swarm, options, handler) => { } if (!mafmt.Circuit.matches(addr)) { - // by default we're reachable over any relay - listenAddrs.push(multiaddr(`/p2p-circuit`).encapsulate(`${addr}/ipfs/${swarm._peerInfo.id.toB58String()}`)) + if (addr.getPeerId() !== null) { + // by default we're reachable over any relay + listenAddrs.push(multiaddr(`/p2p-circuit`).encapsulate(addr)) + } else { + listenAddrs.push(multiaddr(`/p2p-circuit`).encapsulate(`${addr}/ipfs/${swarm._peerInfo.id.toB58String()}`)) + } } else { listenAddrs.push(addr.encapsulate(`/ipfs/${swarm._peerInfo.id.toB58String()}`)) } From 0110a71ef2e1971fba174493c695fa0ae4b66e6b Mon Sep 17 00:00:00 2001 From: Dmitriy Ryajov Date: Tue, 4 Jul 2017 11:43:43 -0700 Subject: [PATCH 15/20] feat: initial reimplementation of /hop /stop as a protobufs --- package.json | 2 +- src/protocol/index.js | 4 ++++ src/protocol/proto.js | 23 +++++++++++++++++++ test/proto.spec.js | 52 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 src/protocol/index.js create mode 100644 src/protocol/proto.js create mode 100644 test/proto.spec.js diff --git a/package.json b/package.json index 71be26a..e0a27ef 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "aegir": "^10.0.0", "chai": "^3.5.0", "pre-commit": "^1.2.2", - "proxyquire": "^1.7.11", "sinon": "^2.1.0" }, "contributors": [], @@ -48,6 +47,7 @@ "multistream-select": "^0.13.4", "peer-id": "^0.8.2", "peer-info": "^0.8.5", + "protocol-buffers": "^3.2.1", "pull-abortable": "^4.1.0", "pull-handshake": "^1.1.4", "pull-stream": "^3.5.1", diff --git a/src/protocol/index.js b/src/protocol/index.js new file mode 100644 index 0000000..d403960 --- /dev/null +++ b/src/protocol/index.js @@ -0,0 +1,4 @@ +'use strict' + +const protobuf = require('protocol-buffers') +module.exports = protobuf(require('./proto.js')) diff --git a/src/protocol/proto.js b/src/protocol/proto.js new file mode 100644 index 0000000..1a0cc81 --- /dev/null +++ b/src/protocol/proto.js @@ -0,0 +1,23 @@ +'use strict' +module.exports = ` +message Circuit { + optional string version = 1; + optional RelayMessage message = 2; + + enum MessageType { + HOP = 1; + STOP = 2; + } + + message Peer { + optional string id = 1; // peer id + repeated string address = 2; // peer's dialable addresses + } + + message RelayMessage { + optional MessageType type = 1; + optional Peer source = 2; + optional Peer dest = 3; + } +} +` diff --git a/test/proto.spec.js b/test/proto.spec.js new file mode 100644 index 0000000..d1337b9 --- /dev/null +++ b/test/proto.spec.js @@ -0,0 +1,52 @@ +/* eslint-env mocha */ +'use strict' + +const expect = require('chai').expect + +const proto = require('../src/protocol') + +describe('protocol', function () { + let msgObject = null + let message = null + + before(() => { + msgObject = { + version: '1.0.0', + message: { + type: proto.Circuit.MessageType.HOP, + source: { + id: 'QmSource', + address: [ + '/p2p-circuit/ipfs/QmSource', + '/p2p-circuit/ipv4/0.0.0.0/9000/ipfs/QmSource', + 'ipv4/0.0.0.0/9000/ipfs/QmSource' + ] + }, + dest: { + id: 'QmDest', + address: [ + '/p2p-circuit/ipfs/QmDest', + '/p2p-circuit/ipv4/1.1.1.1/9000/ipfs/QmDest', + 'ipv4/1.1.1.1/9000/ipfs/QmDest' + ] + } + } + } + + let buff = proto.Circuit.encode(msgObject) + message = proto.Circuit.decode(buff) + }) + + it(`version should match`, () => { + expect(message.version).to.equal('1.0.0') + }) + + it(`should source and dest`, () => { + expect(message.message.source).to.not.be.null + expect(message.message.dest).to.not.be.null + }) + + it(`should encode message`, () => { + expect(message.message).to.deep.equal(msgObject.message) + }) +}) From bef3f9e83aedb399d967a22b609734bd7f1675bc Mon Sep 17 00:00:00 2001 From: Dmitriy Ryajov Date: Sun, 16 Jul 2017 22:55:26 -0700 Subject: [PATCH 16/20] feat: reworking with protobufs --- src/circuit/constants.js | 6 +- src/circuit/dialer.js | 99 ++++++++++++------ src/circuit/hop.js | 161 +++++++++-------------------- src/circuit/onion-dialer.js | 2 +- src/circuit/stop.js | 77 +++++--------- src/circuit/stream-handler.js | 14 ++- src/circuit/utils.js | 57 +++++++++- src/dialer.js | 13 +-- src/listener.js | 44 +++++++- src/multicodec.js | 3 +- src/protocol/proto.js | 47 ++++++--- test/dialer.spec.js | 18 ++-- test/hop.spec.js | 133 +++++++++++++++--------- test/listener.spec.js | 189 ++++++++++++++++++++++++++++++++++ test/proto.spec.js | 47 ++++----- test/stop.spec.js | 84 ++++++--------- 16 files changed, 628 insertions(+), 366 deletions(-) diff --git a/src/circuit/constants.js b/src/circuit/constants.js index 8799d2f..6bb8301 100644 --- a/src/circuit/constants.js +++ b/src/circuit/constants.js @@ -16,13 +16,15 @@ module.exports = { CANT_DIAL_DST: 261, CANT_OPEN_DST_STREAM: 262, CANT_SPEAK_RELAY: 270, - CANT_CONNECT_TO_SELF: 280 + CANT_CONNECT_TO_SELF: 280, + MSG_TO_LONG: 290 }, STOP: { SRC_ADDR_TOO_LONG: 320, DST_ADDR_TOO_LONG: 321, SRC_MULTIADDR_INVALID: 350, - DST_MULTIADDR_INVALID: 351 + DST_MULTIADDR_INVALID: 351, + MSG_TO_LONG: 350 } } } diff --git a/src/circuit/dialer.js b/src/circuit/dialer.js index 0cbfb21..0c965f8 100644 --- a/src/circuit/dialer.js +++ b/src/circuit/dialer.js @@ -1,11 +1,8 @@ 'use strict' -const Buffer = require('safe-buffer').Buffer - const Connection = require('interface-connection').Connection const isFunction = require('lodash.isfunction') const multiaddr = require('multiaddr') -const constants = require('./constants') const once = require('once') const waterfall = require('async/waterfall') const utilsFactory = require('./utils') @@ -16,6 +13,7 @@ const log = debug('libp2p:circuit:dialer') log.err = debug('libp2p:circuit:error:dialer') const multicodec = require('../multicodec') +const proto = require('../protocol') class Dialer { /** @@ -110,51 +108,93 @@ class Dialer { negotiateRelay (relay, dstMa, callback) { dstMa = multiaddr(dstMa) - // TODO: whats the best addr to send? First one seems as good as any. - const srcMa = this.swarm._peerInfo.multiaddrs.toArray()[0] - const relayConn = new Connection() - - let streamHandler = null + const srcMas = this.swarm._peerInfo.multiaddrs.toArray() waterfall([ (cb) => { if (relay instanceof Connection) { - return cb(null, relay) + return cb(null, new StreamHandler(relay)) } return this.dialRelay(this.utils.peerInfoFromMa(relay), cb) }, - (conn, cb) => { + (streamHandler, cb) => { log(`negotiating relay for peer ${dstMa.getPeerId()}`) - streamHandler = new StreamHandler(conn, 1000 * 60) - streamHandler.write([ - new Buffer(dstMa.toString()), - new Buffer(srcMa.toString()) - ], (err) => { - if (err) { - log.err(err) - return cb(err) - } - - cb(null) - }) + streamHandler.write( + proto.CircuitRelay.encode({ + type: proto.CircuitRelay.Type.HOP, + srcPeer: { + id: this.swarm._peerInfo.id.toB58String(), + addrs: srcMas.map((addr) => addr.toString()) + }, + dstPeer: { + id: dstMa.getPeerId(), + addrs: [dstMa.toString()] + } + }), + (err) => { + if (err) { + log.err(err) + return cb(err) + } + + cb(null, streamHandler) + }) }, - (cb) => { + (streamHandler, cb) => { streamHandler.read((err, msg) => { if (err) { log.err(err) return cb(err) } - if (Number(msg.toString()) !== constants.RESPONSE.SUCCESS) { - return cb(new Error(`Got ${msg.toString()} error code trying to dial over relay`)) + const message = proto.CircuitRelay.decode(msg) + if (message.type !== proto.CircuitRelay.Type.STATUS) { + return cb(new Error(`Got invalid message type - ` + + `expected ${proto.CircuitRelay.Type.STATUS} got ${message.type}`)) + } + + if (message.code !== proto.CircuitRelay.Status.SUCCESS) { + return cb(new Error(`Got ${message.code} error code trying to dial over relay`)) } - relayConn.setInnerConn(streamHandler.rest()) - cb(null, relayConn) + cb(null, new Connection(streamHandler.rest())) }) } ], callback) } + /** + * Does the peer support the HOP protocol + * + * @param {PeerInfo} peer + * @param {Function} cb + * @returns {*} + */ + canHop (peer, cb) { + cb = once(cb || (() => {})) + + if (!this.relayPeers.get(this.utils.getB58String(peer))) { + return this.dialRelay(peer, (err, streamHandler) => { + if (err) { + return log.err(err) + } + + streamHandler.write(proto.CircuitRelay.encode({ + type: proto.CircuitRelay.Type.CAN_HOP + }), (err) => { + if (err) { + log.err(err) + return cb(err) + } + + this.relayPeers.set(this.utils.getB58String(peer), peer) + cb(null) + }) + }) + } + + return cb() + } + /** * Dial a relay peer by its PeerInfo * @@ -170,15 +210,14 @@ class Dialer { const relayConn = new Connection() relayConn.setPeerInfo(peer) // attempt to dia the relay so that we have a connection - this.swarm.dial(peer, multicodec.hop, once((err, conn) => { + this.swarm.dial(peer, multicodec.relay, once((err, conn) => { if (err) { log.err(err) return cb(err) } relayConn.setInnerConn(conn) - this.relayPeers.set(this.utils.getB58String(peer), peer) - cb(null, relayConn) + cb(null, new StreamHandler(conn)) })) } } diff --git a/src/circuit/hop.js b/src/circuit/hop.js index 1dfac0e..0efa582 100644 --- a/src/circuit/hop.js +++ b/src/circuit/hop.js @@ -8,20 +8,18 @@ const debug = require('debug') const PeerInfo = require('peer-info') const PeerId = require('peer-id') const EE = require('events').EventEmitter -const multiaddr = require('multiaddr') const constants = require('./constants') const once = require('once') const utilsFactory = require('./utils') const StreamHandler = require('./stream-handler') -const waterfall = require('async/waterfall') const assignInWith = require('lodash/assignInWith') +const proto = require('../protocol') const multicodec = require('./../multicodec') const log = debug('libp2p:swarm:circuit:relay') log.err = debug('libp2p:swarm:circuit:error:relay') -let utils class Hop extends EE { /** * Construct a Circuit object @@ -30,10 +28,14 @@ class Hop extends EE { * either start a relay or hand the relayed connection to * the swarm * - * @param {any} options - configuration for Relay + * @param {Swarm} swarm + * @param {Object} options */ - constructor (options) { + constructor (swarm, options) { super() + this.swarm = swarm + this.peerInfo = this.swarm._peerInfo + this.utils = utilsFactory(swarm) this.config = assignInWith( { active: false, @@ -44,138 +46,69 @@ class Hop extends EE { typeof src === 'undefined' ? false : src }) - this.swarm = null this.active = this.config.active } - _writeErr (streamHandler, errCode, cb) { - errCode = String(errCode) - setImmediate(() => this.emit('circuit:error', errCode)) - streamHandler.write([Buffer.from(errCode)]) - return cb(errCode) - } - - _readSrcAddr (streamHandler, cb) { - streamHandler.read((err, srcMa) => { - if (err) { - log.err(err) - - // TODO: pull-length-prefixed should probably return an `Error` object with an error code - if (typeof err === 'string' && err.includes('size longer than max permitted length of')) { - return this._writeErr(streamHandler, constants.RESPONSE.HOP.SRC_ADDR_TOO_LONG, cb) - } - } - - try { - srcMa = multiaddr(srcMa.toString()) // read the src multiaddr - } catch (err) { - return this._writeErr(streamHandler, constants.RESPONSE.HOP.SRC_MULTIADDR_INVALID, cb) - } - - cb(null, srcMa) - }) - } - - _readDstAddr (streamHandler, cb) { - streamHandler.read((err, dstMa) => { - if (err) { - log.err(err) - - // TODO: pull-length-prefixed should probably return an `Error` object with an error code - if (typeof err === 'string' && err.includes('size longer than max permitted length of')) { - return this._writeErr(streamHandler, constants.RESPONSE.HOP.DST_ADDR_TOO_LONG, cb) - } - } - - try { - dstMa = multiaddr(dstMa.toString()) // read the src multiaddr - } catch (err) { - return this._writeErr(streamHandler, constants.RESPONSE.HOP.DST_MULTIADDR_INVALID, cb) - } - - if (dstMa.getPeerId() === this.swarm._peerInfo.id.toB58String()) { - return this._writeErr(streamHandler, constants.RESPONSE.HOP.CANT_CONNECT_TO_SELF, cb) - } - - cb(null, dstMa) - }) - } - /** - * Mount the relay + * Handle the relay message * - * @param {swarm} swarm - * @return {void} + * @param {CircuitRelay} message + * @param {StreamHandler} streamHandler + * @returns {*} */ - mount (swarm) { + handle (message, streamHandler) { if (!this.config.enabled) { - return + return this.utils.writeResponse(streamHandler, proto.CircuitRelay.Status.HOP_CANT_SPEAK_RELAY) } - this.swarm = swarm - utils = utilsFactory(swarm) - this.swarm.handle(multicodec.hop, (proto, conn) => { - const streamHandler = new StreamHandler(conn, 1000 * 60) - waterfall([ - (cb) => this._readDstAddr(streamHandler, (err, dstMa) => { - if (err) { - return cb(err) - } - - if (!this.active && !utils.isPeerConnected(dstMa.getPeerId())) { - return this._writeErr(streamHandler, constants.RESPONSE.HOP.NO_CONN_TO_DST, cb) - } - - cb(null, dstMa) - }), - (dstMa, cb) => { - this._readSrcAddr(streamHandler, (err, srcMa) => { - cb(err, dstMa, srcMa) - }) - } - ], (err, dstMa, srcMa) => { - if (err || (!dstMa || !srcMa)) { - log.err(`Error handling incoming relay request`, err) - return - } + if (message.type === proto.CircuitRelay.Type.CAN_HOP) { + return this.utils.writeResponse(streamHandler, proto.CircuitRelay.Status.SUCCESS) + } - return this._circuit(streamHandler.rest(), dstMa, srcMa, (err) => { - if (err) { - log.err(err) - setImmediate(() => this.emit('circuit:error', err)) - } - setImmediate(() => this.emit('circuit:success')) - }) + if (message.dstPeer.id === this.peerInfo.id.toB58String()) { + return this.utils.writeResponse(streamHandler, proto.CircuitRelay.Status.HOP_CANT_RELAY_TO_SELF) + } + + this.utils.validateMsg(message, streamHandler, proto.CircuitRelay.Type.HOP, (err) => { + if (err) { + return log(err) + } + + return this._circuit(streamHandler.rest(), message, (err) => { + if (err) { + log.err(err) + setImmediate(() => this.emit('circuit:error', err)) + } + setImmediate(() => this.emit('circuit:success')) }) }) - - this.emit('mounted') } /** * The handler called to process a connection * * @param {Connection} conn - * @param {Multiaddr} dstAddr - * @param {Multiaddr} srcAddr + * @param {CircuitRelay} message * @param {Function} cb - * - * @return {void} + * @returns {*} + * @private */ - _circuit (conn, dstAddr, srcAddr, cb) { - this._dialPeer(dstAddr, (err, dstConn) => { + _circuit (conn, message, cb) { + this._dialPeer(message.dstPeer, (err, dstConn) => { if (err) { - const errStreamHandler = new StreamHandler(conn, 1000 * 60) + const errStreamHandler = new StreamHandler(conn) this._writeErr(errStreamHandler, constants.RESPONSE.CANT_DIAL_DST) pull(pull.empty(), errStreamHandler.rest()) log.err(err) return cb(err) } - const streamHandler = new StreamHandler(dstConn, 1000 * 60) - streamHandler.write([new Buffer(srcAddr.toString())], (err) => { + const streamHandler = new StreamHandler(dstConn) + const stopMsg = Object.assign({}, message) + stopMsg.type = proto.CircuitRelay.Type.STOP + streamHandler.write(proto.CircuitRelay.encode(stopMsg), (err) => { if (err) { - const errStreamHandler = new StreamHandler(conn, 1000 * 60) + const errStreamHandler = new StreamHandler(conn) this._writeErr(errStreamHandler, constants.RESPONSE.CANT_OPEN_DST_STREAM) pull(pull.empty(), errStreamHandler.rest()) @@ -198,15 +131,15 @@ class Hop extends EE { /** * Dial the dest peer and create a circuit * - * @param {Multiaddr} ma + * @param {Multiaddr} dstPeer * @param {Function} callback * @returns {Function|void} * @private */ - _dialPeer (ma, callback) { - const peerInfo = new PeerInfo(PeerId.createFromB58String(ma.getPeerId())) - peerInfo.multiaddrs.add(ma) - this.swarm.dial(peerInfo, multicodec.stop, once((err, conn) => { + _dialPeer (dstPeer, callback) { + const peerInfo = new PeerInfo(PeerId.createFromB58String(dstPeer.id)) + dstPeer.addrs.forEach((a) => peerInfo.multiaddrs.add(a.toString())) + this.swarm.dial(peerInfo, multicodec.relay, once((err, conn) => { if (err) { log.err(err) return callback(err) diff --git a/src/circuit/onion-dialer.js b/src/circuit/onion-dialer.js index 05667a8..603b4d9 100644 --- a/src/circuit/onion-dialer.js +++ b/src/circuit/onion-dialer.js @@ -141,7 +141,7 @@ class OnionDialer extends Dialer { */ _handshake (pi, conn, cb) { const proxyConn = new Connection() - const handler = this.swarm.connHandler(pi, multicodec.hop, proxyConn) + const handler = this.swarm.connHandler(pi, multicodec.relay, proxyConn) handler.handleNew(conn, (err, upgradedConn) => { if (err) { log.err(err) diff --git a/src/circuit/stop.js b/src/circuit/stop.js index 291e99d..e539649 100644 --- a/src/circuit/stop.js +++ b/src/circuit/stop.js @@ -3,12 +3,10 @@ const setImmediate = require('async/setImmediate') const EE = require('events').EventEmitter -const Buffer = require('safe-buffer').Buffer -const waterfall = require('async/waterfall') -const StreamHandler = require('./stream-handler') -const constants = require('./constants') -const multiaddr = require('multiaddr') const Connection = require('interface-connection').Connection +const utilsFactory = require('./utils') +const PeerInfo = require('peer-info') +const proto = require('../protocol') const debug = require('debug') @@ -19,63 +17,34 @@ class Stop extends EE { constructor (swarm) { super() this.swarm = swarm + this.utils = utilsFactory(swarm) } - handle (conn, callback) { + handle (message, streamHandler, callback) { callback = callback || (() => {}) - conn.getPeerInfo((err, peerInfo) => { + return this.utils.validateMsg(message, streamHandler, proto.CircuitRelay.Type.STOP, (err) => { if (err) { - log.err('Failed to identify incoming connection', err) - return callback(err, null) + callback(err) + return log(err) } - const streamHandler = new StreamHandler(conn) - waterfall([ - (cb) => { - streamHandler.read((err, msg) => { - if (err) { - log.err(err) - - if (err.includes('size longer than max permitted length of')) { - const errCode = String(constants.RESPONSE.STOP.SRC_ADDR_TOO_LONG) - setImmediate(() => this.emit('circuit:error', errCode)) - streamHandler.write([Buffer.from(errCode)]) - } - - return cb(err) - } - - let srcMa = null - try { - srcMa = multiaddr(msg.toString()) - } catch (err) { - const errCode = String(constants.RESPONSE.STOP.SRC_MULTIADDR_INVALID) - setImmediate(() => this.emit('circuit:error', errCode)) - streamHandler.write([Buffer.from(errCode)]) - return cb(errCode) - } - - // add the addr we got along with the relay request - peerInfo.multiaddrs.add(srcMa) - cb() - }) - }, - (cb) => { - streamHandler.write([Buffer.from(String(constants.RESPONSE.SUCCESS))], (err) => { - if (err) { - log.err(err) - return cb(err) - } - - const newConn = new Connection(streamHandler.rest(), conn) - newConn.setPeerInfo(peerInfo) - setImmediate(() => this.emit('connection', newConn)) - callback(newConn) - cb() - }) + streamHandler.write(proto.CircuitRelay.encode({ + type: proto.CircuitRelay.Type.STATUS, + code: proto.CircuitRelay.Status.SUCCESS + }), (err) => { + if (err) { + log.err(err) + return callback(err) } - ]) + + const peerInfo = new PeerInfo(message.srcPeer.id) + message.srcPeer.addrs.forEach((addr) => peerInfo.multiaddrs.add(addr.toString())) + const newConn = new Connection(streamHandler.rest()) + newConn.setPeerInfo(peerInfo) + setImmediate(() => this.emit('connection', newConn)) + callback(newConn) + }) }) } } diff --git a/src/circuit/stream-handler.js b/src/circuit/stream-handler.js index 48fe675..29cd8c6 100644 --- a/src/circuit/stream-handler.js +++ b/src/circuit/stream-handler.js @@ -20,7 +20,7 @@ class StreamHandler { this.conn = conn this.stream = null this.shake = null - this.maxLength = maxLength || 1024 + this.maxLength = maxLength || 4096 this.stream = handshake({timeout: timeout || 1000 * 60}) this.shake = this.stream.handshake @@ -69,12 +69,13 @@ class StreamHandler { } pull( - pull.values(msg), + pull.values([msg]), lp.encode(), pull.collect((err, encoded) => { if (err) { log.err(err) this.shake.abort(err) + return cb(err) } encoded.forEach((e) => this.shake.write(e)) @@ -83,6 +84,15 @@ class StreamHandler { ) } + /** + * Get the raw Connection + * + * @returns {null|Connection|*} + */ + getRawConn () { + return this.conn + } + /** * Return the handshake rest stream and invalidate handler * diff --git a/src/circuit/utils.js b/src/circuit/utils.js index df2c6d3..54b283f 100644 --- a/src/circuit/utils.js +++ b/src/circuit/utils.js @@ -3,6 +3,7 @@ const multiaddr = require('multiaddr') const PeerInfo = require('peer-info') const PeerId = require('peer-id') +const proto = require('../protocol') module.exports = function (swarm) { /** @@ -66,9 +67,63 @@ module.exports = function (swarm) { return swarm.muxedConns[peerId] || swarm.conns[peerId] } + /** + * Write a response + * + * @param {StreamHandler} streamHandler + * @param {CircuitRelay.Status} status + * @param {Function} cb + * @returns {*} + */ + function writeResponse (streamHandler, status, cb) { + cb = cb || (() => {}) + streamHandler.write(proto.CircuitRelay.encode({ + type: proto.CircuitRelay.Type.STATUS, + code: status + })) + return cb(status) + } + + /** + * Validate incomming HOP/STOP message + * + * @param {CircuitRelay} msg + * @param {StreamHandler} streamHandler + * @param {CircuitRelay.Type} type + * @returns {*} + * @param {Function} cb + */ + function validateMsg (msg, streamHandler, type, cb) { + try { + msg.dstPeer.addrs.forEach((addr) => { + return multiaddr(addr.toString()) + }) + } catch (err) { + writeResponse(streamHandler, type === proto.CircuitRelay.Type.HOP + ? proto.CircuitRelay.Status.HOP_DST_MULTIADDR_INVALID + : proto.CircuitRelay.Status.STOP_DST_MULTIADDR_INVALID) + return cb(err) + } + + try { + msg.srcPeer.addrs.forEach((addr) => { + return multiaddr(addr.toString()) + }) + } catch (err) { + writeResponse(streamHandler, type === proto.CircuitRelay.Type.HOP + ? proto.CircuitRelay.Status.HOP_SRC_MULTIADDR_INVALID + : proto.CircuitRelay.Status.STOP_SRC_MULTIADDR_INVALID) + return cb(err) + } + + return cb(null) + } + return { getB58String: getB58String, peerInfoFromMa: peerInfoFromMa, - isPeerConnected: isPeerConnected + isPeerConnected: isPeerConnected, + validateMsg: validateMsg, + writeResponse: writeResponse } } diff --git a/src/dialer.js b/src/dialer.js index 51fc6be..ec94912 100644 --- a/src/dialer.js +++ b/src/dialer.js @@ -15,33 +15,34 @@ const createListener = require('./listener') class Dialer { /** * Creates an instance of Dialer. + * * @param {Swarm} swarm - the swarm * @param {any} options - config options * * @memberOf Dialer */ constructor (swarm, options) { - options = options || {} + this.options = options || {} this.swarm = swarm this.dialer = null this.utils = utilsFactory(swarm) + this.peerInfo = this.swarm._peerInfo // get all the relay addresses for this swarm - const relays = this.filter(swarm._peerInfo.multiaddrs.toArray()) + const relays = this.filter(this.peerInfo.multiaddrs.toArray()) // if no explicit relays, add a default relay addr if (relays.length === 0) { - this.swarm - ._peerInfo + this.peerInfo .multiaddrs - .add(`/p2p-circuit/ipfs/${this.swarm._peerInfo.id.toB58String()}`) + .add(`/p2p-circuit/ipfs/${this.peerInfo.id.toB58String()}`) } // TODO: add flag for other types of dealers, ie telescope this.dialer = new OnionDialer(swarm, options) - this.swarm.on('peer-mux-established', this.dialer.dialRelay.bind(this.dialer)) + this.swarm.on('peer-mux-established', this.dialer.canHop.bind(this.dialer)) this.swarm.on('peer-mux-closed', (peerInfo) => { this.dialer.relayPeers.delete(peerInfo.id.toB58String()) }) diff --git a/src/listener.js b/src/listener.js index 64a8b5f..87d8a6e 100644 --- a/src/listener.js +++ b/src/listener.js @@ -7,21 +7,57 @@ const EE = require('events').EventEmitter const multiaddr = require('multiaddr') const mafmt = require('mafmt') const Stop = require('./circuit/stop') +const Hop = require('./circuit/hop') +const proto = require('./protocol') +const utilsFactory = require('./circuit/utils') + +const StreamHandler = require('./circuit/stream-handler') const debug = require('debug') const log = debug('libp2p:circuit:listener') log.err = debug('libp2p:circuit:error:listener') -module.exports = (swarm, options, handler) => { +module.exports = (swarm, options, connHandler) => { const listener = new EE() - const stopHandler = new Stop(swarm) + const utils = utilsFactory(swarm) + + listener.stopHandler = new Stop(swarm) + listener.hopHandler = new Hop(swarm, options.circuit) listener.listen = (ma, callback) => { callback = callback || (() => {}) - swarm.handle(multicodec.stop, (proto, conn) => { - stopHandler.handle(conn, handler) + swarm.handle(multicodec.relay, (relayProto, conn) => { + const streamHandler = new StreamHandler(conn) + streamHandler.read((err, msg) => { + if (err) { + log.err(err) + return + } + + let request = null + try { + request = proto.CircuitRelay.decode(msg) + } catch (err) { + return utils.writeResponse(streamHandler, proto.CircuitRelay.Status.INVALID_MSG_TYPE) + } + + switch (request.type) { + case proto.CircuitRelay.Type.CAN_HOP: + case proto.CircuitRelay.Type.HOP: { + return listener.hopHandler.handle(request, streamHandler) + } + + case proto.CircuitRelay.Type.STOP: { + return listener.stopHandler.handle(request, streamHandler, connHandler) + } + + default: { + return utils.writeResponse(streamHandler, proto.CircuitRelay.Status.INVALID_MSG_TYPE) + } + } + }) }) setImmediate(() => listener.emit('listen')) diff --git a/src/multicodec.js b/src/multicodec.js index 3055325..bcdb978 100644 --- a/src/multicodec.js +++ b/src/multicodec.js @@ -1,6 +1,5 @@ 'use strict' module.exports = { - hop: '/ipfs/relay/circuit/1.0.0/hop', - stop: '/ipfs/relay/circuit/1.0.0/stop' + relay: '/libp2p/circuit/relay/0.1.0' } diff --git a/src/protocol/proto.js b/src/protocol/proto.js index 1a0cc81..4a9e29f 100644 --- a/src/protocol/proto.js +++ b/src/protocol/proto.js @@ -1,23 +1,42 @@ 'use strict' module.exports = ` -message Circuit { - optional string version = 1; - optional RelayMessage message = 2; - - enum MessageType { +message CircuitRelay { + + enum Status { + SUCCESS = 100; + INVALID_MSG_TYPE = 101; + HOP_SRC_ADDR_TOO_LONG= 220; + HOP_DST_ADDR_TOO_LONG= 221; + HOP_SRC_MULTIADDR_INVALID= 250; + HOP_DST_MULTIADDR_INVALID= 251; + HOP_NO_CONN_TO_DST = 260; + HOP_CANT_DIAL_DST = 261; + HOP_CANT_OPEN_DST_STREAM = 262; + HOP_CANT_SPEAK_RELAY = 270; + HOP_CANT_RELAY_TO_SELF = 280; + STOP_SRC_ADDR_TOO_LONG = 320; + STOP_DST_ADDR_TOO_LONG = 321; + STOP_SRC_MULTIADDR_INVALID = 350; + STOP_DST_MULTIADDR_INVALID = 351; + } + + enum Type { // RPC identifier, either HOP, STOP or STATUS HOP = 1; STOP = 2; + STATUS = 3; + CAN_HOP = 4; } - + message Peer { - optional string id = 1; // peer id - repeated string address = 2; // peer's dialable addresses - } - - message RelayMessage { - optional MessageType type = 1; - optional Peer source = 2; - optional Peer dest = 3; + required bytes id = 1; // peer id + repeated bytes addrs = 2; // peer's known addresses } + + optional Type type = 1; // Type of the message + + optional Peer srcPeer = 2; // srcPeer and dstPeer are used when Type is HOP or STATUS + optional Peer dstPeer = 3; + + optional Status code = 4; // Status code, used when Type is STATUS } ` diff --git a/test/dialer.spec.js b/test/dialer.spec.js index 17efa13..70ab44a 100644 --- a/test/dialer.spec.js +++ b/test/dialer.spec.js @@ -5,13 +5,13 @@ const Dialer = require('../src/circuit/dialer') const nodes = require('./fixtures/nodes') const Connection = require('interface-connection').Connection const multiaddr = require('multiaddr') -const constants = require('../src/circuit/constants') const handshake = require('pull-handshake') const PeerInfo = require('peer-info') const PeerId = require('peer-id') const waterfall = require('async/waterfall') const pull = require('pull-stream') const lp = require('pull-length-prefixed') +const proto = require('../src/protocol') const sinon = require('sinon') const expect = require('chai').expect @@ -32,7 +32,7 @@ describe('dialer tests', function () { dialer.negotiateRelay.reset() }) - it(`negotiate a relay on all available relays`, function (done) { + it(`negotiate a circuit on all available relays`, function (done) { const dstMa = multiaddr(`/ipfs/${nodes.node4.id}`) dialer.negotiateRelay.callsFake(function (conn, dstMa, callback) { if (conn === dialer.relayPeers.get(nodes.node3.id)) { @@ -105,9 +105,12 @@ describe('dialer tests', function () { it(`write the correct dst addr`, function (done) { lp.decodeFromReader(shake, (err, msg) => { - shake.write(new Buffer(String(constants.RESPONSE.SUCCESS))) + shake.write(proto.CircuitRelay.encode({ + type: proto.CircuitRelay.Type.STATUS, + code: proto.CircuitRelay.Status.SUCCESS + })) expect(err).to.be.null - expect(msg.toString()).to.be.equal(`${dstMa.toString()}`) + expect(proto.CircuitRelay.decode(msg).dstPeer.addrs.toString()).to.be.equal(`${dstMa.toString()}`) done() }) }) @@ -116,7 +119,7 @@ describe('dialer tests', function () { callback.callsFake((err, msg) => { expect(err).to.not.be.null expect(err).to.be.an.instanceOf(Error) - expect(err.message).to.be.equal(`Got 500 error code trying to dial over relay`) + expect(err.message).to.be.equal(`Got 101 error code trying to dial over relay`) expect(callback.calledOnce).to.be.ok done() }) @@ -125,7 +128,10 @@ describe('dialer tests', function () { if (err) return done(err) pull( - pull.values([Buffer.from(String(500))]), // send arbitrary non 200 code + pull.values([proto.CircuitRelay.encode({ + type: proto.CircuitRelay.Type.STATUS, + code: proto.CircuitRelay.Status.INVALID_MSG_TYPE + })]), // send arbitrary non 200 code lp.encode(), pull.collect((err, encoded) => { expect(err).to.be.null diff --git a/test/hop.spec.js b/test/hop.spec.js index e0322c6..5c07f69 100644 --- a/test/hop.spec.js +++ b/test/hop.spec.js @@ -2,39 +2,32 @@ 'use strict' const Hop = require('../src/circuit/hop') -const Dialer = require('../src/circuit/dialer') const nodes = require('./fixtures/nodes') const Connection = require('interface-connection').Connection -const multiaddr = require('multiaddr') -const multicodec = require('../src/multicodec') -const constants = require('../src/circuit/constants') const handshake = require('pull-handshake') const waterfall = require('async/waterfall') const PeerInfo = require('peer-info') const PeerId = require('peer-id') -const longaddr = require('./fixtures/long-address') +const lp = require('pull-length-prefixed') +const proto = require('../src/protocol') +const StreamHandler = require('../src/circuit/stream-handler') const sinon = require('sinon') const expect = require('chai').expect describe('relay', function () { describe(`handle circuit requests`, function () { - const dialer = sinon.createStubInstance(Dialer) - let relay let swarm let fromConn - let toConn let stream let shake - let handlerSpy beforeEach(function (done) { stream = handshake({timeout: 1000 * 60}) shake = stream.handshake fromConn = new Connection(stream) - fromConn.setPeerInfo(new PeerInfo(PeerId.createFromB58String('QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE'))) - toConn = new Connection(shake.rest()) + fromConn.setPeerInfo(new PeerInfo(PeerId.createFromB58String('QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA'))) waterfall([ (cb) => PeerId.createFromJSON(nodes.node4, cb), @@ -43,25 +36,17 @@ describe('relay', function () { peer.multiaddrs.add('/p2p-circuit/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE') swarm = { _peerInfo: peer, - handle: sinon.spy((proto, h) => { - handlerSpy = sinon.spy(h) - }), conns: { QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE: new Connection() } } - dialer.swarm = swarm cb() } ], () => { - relay = new Hop({enabled: true}) - relay.mount(swarm) // mount the swarm + relay = new Hop(swarm, {enabled: true}) relay._circuit = sinon.stub() - relay._circuit.callsArg(3, null, toConn) - - dialer.relayConns = new Map() - dialer.negotiateRelay.callThrough() + relay._circuit.callsArg(2, null, new Connection()) done() }) }) @@ -71,49 +56,99 @@ describe('relay', function () { }) it(`handle a valid circuit request`, function (done) { - relay.active = true + let relayMsg = { + type: proto.CircuitRelay.Type.HOP, + srcPeer: { + id: `QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`, + addrs: [`/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`] + }, + dstPeer: { + id: `QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`, + addrs: [`/ipfs/QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`] + } + } + relay.on('circuit:success', () => { - expect(relay._circuit.calledWith(sinon.match.any, dstMa)).to.be.ok + expect(relay._circuit.calledWith(sinon.match.any, relayMsg)).to.be.ok done() }) - let dstMa = multiaddr(`/ip4/0.0.0.0/tcp/9033/ws/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`) - dialer.negotiateRelay(fromConn, dstMa, () => {}) - handlerSpy(multicodec.hop, toConn) + relay.handle(relayMsg, new StreamHandler(fromConn)) }) - // it(`fail dialing to invalid multiaddr`, function () { - // // TODO: implement without relying on negotiateRelay - // }) - it(`not dial to self`, function (done) { - let dstMa = multiaddr(`/ipfs/${nodes.node4.id}`) - dialer.negotiateRelay(fromConn, dstMa, (err, newConn) => { - expect(err).to.not.be.null - expect(err).to.be.an.instanceOf(Error) - expect(err.message) - .to - .equal(`Got ${constants.RESPONSE.HOP.CANT_CONNECT_TO_SELF} error code trying to dial over relay`) - expect(newConn).to.be.undefined + let relayMsg = { + type: proto.CircuitRelay.Type.HOP, + srcPeer: { + id: `QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`, + addrs: [`/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`] + }, + dstPeer: { + id: `QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`, + addrs: [`/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`] + } + } + + lp.decodeFromReader(shake, (err, msg) => { + expect(err).to.be.null + + const response = proto.CircuitRelay.decode(msg) + expect(response.code).to.equal(proto.CircuitRelay.Status.HOP_CANT_RELAY_TO_SELF) + expect(response.type).to.equal(proto.CircuitRelay.Type.STATUS) done() }) - handlerSpy(multicodec.hop, toConn) + relay.handle(relayMsg, new StreamHandler(fromConn)) }) - it(`fail on address exceeding 1024 bytes`, function (done) { - let dstMa = multiaddr(longaddr.toString()) - dialer.negotiateRelay(fromConn, dstMa, (err, newConn) => { - expect(err).to.not.be.null - expect(err).to.be.an.instanceOf(Error) - expect(err.message) - .to - .equal(`Got ${constants.RESPONSE.HOP.DST_ADDR_TOO_LONG} error code trying to dial over relay`) - expect(newConn).to.be.undefined + it(`fail on invalid src address`, function (done) { + let relayMsg = { + type: proto.CircuitRelay.Type.HOP, + srcPeer: { + id: `sdfkjsdnfkjdsb`, + addrs: [`sdfkjsdnfkjdsb`] + }, + dstPeer: { + id: `QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`, + addrs: [`/ipfs/QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`] + } + } + + lp.decodeFromReader(shake, (err, msg) => { + expect(err).to.be.null + + const response = proto.CircuitRelay.decode(msg) + expect(response.code).to.equal(proto.CircuitRelay.Status.HOP_SRC_MULTIADDR_INVALID) + expect(response.type).to.equal(proto.CircuitRelay.Type.STATUS) + done() + }) + + relay.handle(relayMsg, new StreamHandler(fromConn)) + }) + + it(`fail on invalid dst address`, function (done) { + let relayMsg = { + type: proto.CircuitRelay.Type.HOP, + srcPeer: { + id: `QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`, + addrs: [`/ipfs/QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`] + }, + dstPeer: { + id: `sdfkjsdnfkjdsb`, + addrs: [`sdfkjsdnfkjdsb`] + } + } + + lp.decodeFromReader(shake, (err, msg) => { + expect(err).to.be.null + + const response = proto.CircuitRelay.decode(msg) + expect(response.code).to.equal(proto.CircuitRelay.Status.HOP_DST_MULTIADDR_INVALID) + expect(response.type).to.equal(proto.CircuitRelay.Type.STATUS) done() }) - handlerSpy(multicodec.hop, toConn) + relay.handle(relayMsg, new StreamHandler(fromConn)) }) }) }) diff --git a/test/listener.spec.js b/test/listener.spec.js index 54071d9..084ecc6 100644 --- a/test/listener.spec.js +++ b/test/listener.spec.js @@ -8,8 +8,197 @@ const PeerInfo = require('peer-info') const PeerId = require('peer-id') const multiaddr = require('multiaddr') const expect = require('chai').expect +const sinon = require('sinon') +const handshake = require('pull-handshake') +const Connection = require('interface-connection').Connection +const proto = require('../src/protocol') +const lp = require('pull-length-prefixed') +const pull = require('pull-stream') +const multicodec = require('../src/multicodec') describe('listener', function () { + describe(`listen`, function () { + let swarm = null + let handlerSpy = null + let listener = null + let stream = null + let shake = null + let conn = null + + beforeEach(function (done) { + stream = handshake({timeout: 1000 * 60}) + shake = stream.handshake + conn = new Connection(stream) + conn.setPeerInfo(new PeerInfo(PeerId.createFromB58String('QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE'))) + + waterfall([ + (cb) => PeerId.createFromJSON(nodes.node4, cb), + (peerId, cb) => PeerInfo.create(peerId, cb), + (peer, cb) => { + swarm = { + _peerInfo: peer, + handle: sinon.spy((proto, h) => { + handlerSpy = sinon.spy(h) + }), + conns: { + QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE: new Connection() + } + } + + listener = Listener(swarm, {}, () => {}) + listener.listen() + cb() + } + ], done) + }) + + afterEach(() => { + listener = null + }) + + it(`should handle HOP`, function (done) { + handlerSpy(multicodec.relay, conn) + + let relayMsg = { + type: proto.CircuitRelay.Type.HOP, + srcPeer: { + id: `QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`, + addrs: [`/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`] + }, + dstPeer: { + id: `QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`, + addrs: [`/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`] + } + } + + listener.hopHandler.handle = (message, conn) => { + expect(message.type).to.equal(proto.CircuitRelay.Type.HOP) + + expect(message.srcPeer.id.toString()).to.equal(relayMsg.srcPeer.id) + expect(message.srcPeer.addrs[0].toString()).to.equal(relayMsg.srcPeer.addrs[0]) + + expect(message.dstPeer.id.toString()).to.equal(relayMsg.dstPeer.id) + expect(message.dstPeer.addrs[0].toString()).to.equal(relayMsg.dstPeer.addrs[0]) + + done() + } + + pull( + pull.values([proto.CircuitRelay.encode(relayMsg)]), + lp.encode(), + pull.collect((err, encoded) => { + expect(err).to.be.null + encoded.forEach((e) => shake.write(e)) + }) + ) + }) + + it(`should handle STOP`, function (done) { + handlerSpy(multicodec.relay, conn) + + let relayMsg = { + type: proto.CircuitRelay.Type.STOP, + srcPeer: { + id: `QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`, + addrs: [`/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`] + }, + dstPeer: { + id: `QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`, + addrs: [`/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`] + } + } + + listener.stopHandler.handle = (message, conn) => { + expect(message.type).to.equal(proto.CircuitRelay.Type.STOP) + + expect(message.srcPeer.id.toString()).to.equal(relayMsg.srcPeer.id) + expect(message.srcPeer.addrs[0].toString()).to.equal(relayMsg.srcPeer.addrs[0]) + + expect(message.dstPeer.id.toString()).to.equal(relayMsg.dstPeer.id) + expect(message.dstPeer.addrs[0].toString()).to.equal(relayMsg.dstPeer.addrs[0]) + + done() + } + + pull( + pull.values([proto.CircuitRelay.encode(relayMsg)]), + lp.encode(), + pull.collect((err, encoded) => { + expect(err).to.be.null + encoded.forEach((e) => shake.write(e)) + }) + ) + }) + + it(`should handle CAN_HOP`, function (done) { + handlerSpy(multicodec.relay, conn) + + let relayMsg = { + type: proto.CircuitRelay.Type.CAN_HOP, + srcPeer: { + id: `QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`, + addrs: [`/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`] + }, + dstPeer: { + id: `QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`, + addrs: [`/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`] + } + } + + listener.hopHandler.handle = (message, conn) => { + expect(message.type).to.equal(proto.CircuitRelay.Type.CAN_HOP) + + expect(message.srcPeer.id.toString()).to.equal(relayMsg.srcPeer.id) + expect(message.srcPeer.addrs[0].toString()).to.equal(relayMsg.srcPeer.addrs[0]) + + expect(message.dstPeer.id.toString()).to.equal(relayMsg.dstPeer.id) + expect(message.dstPeer.addrs[0].toString()).to.equal(relayMsg.dstPeer.addrs[0]) + + done() + } + + pull( + pull.values([proto.CircuitRelay.encode(relayMsg)]), + lp.encode(), + pull.collect((err, encoded) => { + expect(err).to.be.null + encoded.forEach((e) => shake.write(e)) + }) + ) + }) + + it(`should handle invalid message correctly`, function (done) { + handlerSpy(multicodec.relay, conn) + + let relayMsg = { + type: 100000, + srcPeer: { + id: `QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`, + addrs: [`/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`] + }, + dstPeer: { + id: `QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`, + addrs: [`/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`] + } + } + + pull( + pull.values([Buffer.from([relayMsg])]), + lp.encode(), + pull.collect((err, encoded) => { + expect(err).to.be.null + encoded.forEach((e) => shake.write(e)) + }), + lp.decodeFromReader(shake, {maxLength: this.maxLength}, (err, msg) => { + expect(err).to.be.null + expect(proto.CircuitRelay.decode(msg).type).to.equal(proto.CircuitRelay.Type.STATUS) + expect(proto.CircuitRelay.decode(msg).code).to.equal(proto.CircuitRelay.Status.INVALID_MSG_TYPE) + done() + }) + ) + }) + }) + describe(`getAddrs`, function () { let swarm = null let listener = null diff --git a/test/proto.spec.js b/test/proto.spec.js index d1337b9..da93f6c 100644 --- a/test/proto.spec.js +++ b/test/proto.spec.js @@ -11,39 +11,32 @@ describe('protocol', function () { before(() => { msgObject = { - version: '1.0.0', - message: { - type: proto.Circuit.MessageType.HOP, - source: { - id: 'QmSource', - address: [ - '/p2p-circuit/ipfs/QmSource', - '/p2p-circuit/ipv4/0.0.0.0/9000/ipfs/QmSource', - 'ipv4/0.0.0.0/9000/ipfs/QmSource' - ] - }, - dest: { - id: 'QmDest', - address: [ - '/p2p-circuit/ipfs/QmDest', - '/p2p-circuit/ipv4/1.1.1.1/9000/ipfs/QmDest', - 'ipv4/1.1.1.1/9000/ipfs/QmDest' - ] - } + type: proto.CircuitRelay.Type.HOP, + srcPeer: { + id: 'QmSource', + addrs: [ + '/p2p-circuit/ipfs/QmSource', + '/p2p-circuit/ipv4/0.0.0.0/9000/ipfs/QmSource', + 'ipv4/0.0.0.0/9000/ipfs/QmSource' + ] + }, + dstPeer: { + id: 'QmDest', + addrs: [ + '/p2p-circuit/ipfs/QmDest', + '/p2p-circuit/ipv4/1.1.1.1/9000/ipfs/QmDest', + 'ipv4/1.1.1.1/9000/ipfs/QmDest' + ] } } - let buff = proto.Circuit.encode(msgObject) - message = proto.Circuit.decode(buff) - }) - - it(`version should match`, () => { - expect(message.version).to.equal('1.0.0') + let buff = proto.CircuitRelay.encode(msgObject) + message = proto.CircuitRelay.decode(buff) }) it(`should source and dest`, () => { - expect(message.message.source).to.not.be.null - expect(message.message.dest).to.not.be.null + expect(message.srcPeer).to.not.be.null + expect(message.dstPeer).to.not.be.null }) it(`should encode message`, () => { diff --git a/test/stop.spec.js b/test/stop.spec.js index 3685afe..ed2c69e 100644 --- a/test/stop.spec.js +++ b/test/stop.spec.js @@ -4,14 +4,12 @@ const Stop = require('../src/circuit/stop') const nodes = require('./fixtures/nodes') const Connection = require('interface-connection').Connection -const constants = require('../src/circuit/constants') const handshake = require('pull-handshake') const waterfall = require('async/waterfall') const PeerInfo = require('peer-info') const PeerId = require('peer-id') -const lp = require('pull-length-prefixed') -const pull = require('pull-stream') -const longaddr = require('./fixtures/long-address') +const StreamHandler = require('../src/circuit/stream-handler') +const proto = require('../src/protocol') const expect = require('chai').expect @@ -22,11 +20,9 @@ describe('stop', function () { let swarm let conn let stream - let shake beforeEach(function (done) { stream = handshake({timeout: 1000 * 60}) - shake = stream.handshake conn = new Connection(stream) conn.setPeerInfo(new PeerInfo(PeerId.createFromB58String('QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE'))) @@ -49,57 +45,37 @@ describe('stop', function () { }) it(`handle a valid request`, function (done) { - pull( - pull.values([Buffer.from(`/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`)]), - lp.encode(), - pull.collect((err, encoded) => { - expect(err).to.be.null - - shake.write(encoded[0]) - lp.decodeFromReader(shake, (err, msg) => { - expect(err).to.be.null - expect(msg.toString()).to.equal(String(constants.RESPONSE.SUCCESS)) - done() - }) - }) - ) - stopHandler.handle(conn) + stopHandler.handle({ + type: proto.CircuitRelay.Type.STOP, + srcPeer: { + id: `QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`, + addrs: [`/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`] + }, + dstPeer: { + id: `QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`, + addrs: [`/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`] + } + }, new StreamHandler(conn), (conn) => { + expect(conn).to.be.not.null + done() + }) }) it(`handle request with invalid multiaddress`, function (done) { - pull( - pull.values([Buffer.from(`sdfsddfds`)]), - lp.encode(), - pull.collect((err, encoded) => { - expect(err).to.be.null - - shake.write(encoded[0]) - lp.decodeFromReader(shake, (err, msg) => { - expect(err).to.be.null - expect(msg.toString()).to.equal(String(constants.RESPONSE.STOP.SRC_MULTIADDR_INVALID)) - done() - }) - }) - ) - stopHandler.handle(conn) - }) - - it(`handle request with a long multiaddress`, function (done) { - pull( - pull.values([longaddr]), - lp.encode(), - pull.collect((err, encoded) => { - expect(err).to.be.null - - shake.write(encoded[0]) - lp.decodeFromReader(shake, (err, msg) => { - expect(err).to.be.null - expect(msg.toString()).to.equal(String(constants.RESPONSE.STOP.SRC_ADDR_TOO_LONG)) - done() - }) - }) - ) - stopHandler.handle(conn) + stopHandler.handle({ + type: proto.CircuitRelay.Type.STOP, + srcPeer: { + id: `QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`, + addrs: [`dsfsdfsdf`] + }, + dstPeer: { + id: `QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`, + addrs: [`sdflksdfndsklfnlkdf`] + } + }, new StreamHandler(conn), (err, conn) => { + expect(err).to.be.not.null + done() + }) }) }) }) From b6ce1892cfb9351148f69f0888e308fdb78cad0a Mon Sep 17 00:00:00 2001 From: Dmitriy Ryajov Date: Mon, 24 Jul 2017 09:57:16 -0700 Subject: [PATCH 17/20] feat: moving SUCCESS response to HOP from STOP --- src/circuit/hop.js | 44 ++++++++++++++++++++++++++++---------------- src/circuit/stop.js | 22 ++++++---------------- 2 files changed, 34 insertions(+), 32 deletions(-) diff --git a/src/circuit/hop.js b/src/circuit/hop.js index 0efa582..9164f37 100644 --- a/src/circuit/hop.js +++ b/src/circuit/hop.js @@ -103,27 +103,39 @@ class Hop extends EE { return cb(err) } - const streamHandler = new StreamHandler(dstConn) - const stopMsg = Object.assign({}, message) - stopMsg.type = proto.CircuitRelay.Type.STOP - streamHandler.write(proto.CircuitRelay.encode(stopMsg), (err) => { + const srcStreamHandler = new StreamHandler(conn) + srcStreamHandler.write(proto.CircuitRelay.encode({ + type: proto.CircuitRelay.Type.STATUS, + code: proto.CircuitRelay.Status.SUCCESS + }), (err) => { if (err) { - const errStreamHandler = new StreamHandler(conn) - this._writeErr(errStreamHandler, constants.RESPONSE.CANT_OPEN_DST_STREAM) - pull(pull.empty(), errStreamHandler.rest()) - log.err(err) return cb(err) } - // circuit the src and dst streams - pull( - conn, - streamHandler.rest(), - conn - ) - - cb() + const streamHandler = new StreamHandler(dstConn) + const stopMsg = Object.assign({}, message) + stopMsg.type = proto.CircuitRelay.Type.STOP + streamHandler.write(proto.CircuitRelay.encode(stopMsg), (err) => { + if (err) { + const errStreamHandler = new StreamHandler(conn) + this._writeErr(errStreamHandler, constants.RESPONSE.CANT_OPEN_DST_STREAM) + pull(pull.empty(), errStreamHandler.rest()) + + log.err(err) + return cb(err) + } + + const srcConn = srcStreamHandler.rest() + // circuit the src and dst streams + pull( + srcConn, + streamHandler.rest(), + srcConn + ) + + cb() + }) }) }) } diff --git a/src/circuit/stop.js b/src/circuit/stop.js index e539649..a302471 100644 --- a/src/circuit/stop.js +++ b/src/circuit/stop.js @@ -29,22 +29,12 @@ class Stop extends EE { return log(err) } - streamHandler.write(proto.CircuitRelay.encode({ - type: proto.CircuitRelay.Type.STATUS, - code: proto.CircuitRelay.Status.SUCCESS - }), (err) => { - if (err) { - log.err(err) - return callback(err) - } - - const peerInfo = new PeerInfo(message.srcPeer.id) - message.srcPeer.addrs.forEach((addr) => peerInfo.multiaddrs.add(addr.toString())) - const newConn = new Connection(streamHandler.rest()) - newConn.setPeerInfo(peerInfo) - setImmediate(() => this.emit('connection', newConn)) - callback(newConn) - }) + const peerInfo = new PeerInfo(message.srcPeer.id) + message.srcPeer.addrs.forEach((addr) => peerInfo.multiaddrs.add(addr.toString())) + const newConn = new Connection(streamHandler.rest()) + newConn.setPeerInfo(peerInfo) + setImmediate(() => this.emit('connection', newConn)) + callback(newConn) }) } } From be124895f26fc3481ffff06f2762900e2a2898e9 Mon Sep 17 00:00:00 2001 From: Dmitriy Ryajov Date: Wed, 2 Aug 2017 12:07:07 -0700 Subject: [PATCH 18/20] feat: removing multihop (onion) dialing --- src/{dialer.js => circuit.js} | 40 ++++---- src/circuit/constants.js | 30 ------ src/circuit/dialer.js | 37 +++++++- src/circuit/hop.js | 5 +- src/circuit/onion-dialer.js | 161 ------------------------------- src/index.js | 7 +- test/onion-dialer.spec.js | 172 ---------------------------------- 7 files changed, 53 insertions(+), 399 deletions(-) rename src/{dialer.js => circuit.js} (75%) delete mode 100644 src/circuit/constants.js delete mode 100644 src/circuit/onion-dialer.js delete mode 100644 test/onion-dialer.spec.js diff --git a/src/dialer.js b/src/circuit.js similarity index 75% rename from src/dialer.js rename to src/circuit.js index ec94912..d94613b 100644 --- a/src/dialer.js +++ b/src/circuit.js @@ -3,7 +3,7 @@ const mafmt = require('mafmt') const multiaddr = require('multiaddr') -const OnionDialer = require('./circuit/onion-dialer') +const CircuitDialer = require('./circuit/dialer') const utilsFactory = require('./circuit/utils') const debug = require('debug') @@ -12,7 +12,7 @@ log.err = debug('libp2p:circuit:error:transportdialer') const createListener = require('./listener') -class Dialer { +class Circuit { /** * Creates an instance of Dialer. * @@ -28,26 +28,22 @@ class Dialer { this.dialer = null this.utils = utilsFactory(swarm) this.peerInfo = this.swarm._peerInfo - - // get all the relay addresses for this swarm - const relays = this.filter(this.peerInfo.multiaddrs.toArray()) + this.relays = this.filter(this.peerInfo.multiaddrs.toArray()) // if no explicit relays, add a default relay addr - if (relays.length === 0) { + if (this.relays.length === 0) { this.peerInfo .multiaddrs .add(`/p2p-circuit/ipfs/${this.peerInfo.id.toB58String()}`) } // TODO: add flag for other types of dealers, ie telescope - this.dialer = new OnionDialer(swarm, options) + this.dialer = new CircuitDialer(swarm, options) this.swarm.on('peer-mux-established', this.dialer.canHop.bind(this.dialer)) this.swarm.on('peer-mux-closed', (peerInfo) => { this.dialer.relayPeers.delete(peerInfo.id.toB58String()) }) - - this._dialSwarmRelays(relays) } /** @@ -56,18 +52,16 @@ class Dialer { * @param {Array} relays * @return {void} */ - _dialSwarmRelays (relays) { + _dialSwarmRelays () { // if we have relay addresses in swarm config, then dial those relays - this.swarm.on('listening', () => { - relays.forEach((relay) => { - let relaySegments = relay - .toString() - .split('/p2p-circuit') - .filter(segment => segment.length) - - relaySegments.forEach((relaySegment) => { - this.dialer.dialRelay(this.utils.peerInfoFromMa(multiaddr(relaySegment))) - }) + this.relays.forEach((relay) => { + let relaySegments = relay + .toString() + .split('/p2p-circuit') + .filter(segment => segment.length) + + relaySegments.forEach((relaySegment) => { + this.dialer.dialRelay(this.utils.peerInfoFromMa(multiaddr(relaySegment))) }) }) } @@ -99,7 +93,9 @@ class Dialer { options = this.options || {} } - return createListener(this.swarm, options, handler) + const listener = createListener(this.swarm, options, handler) + listener.on('listen', this._dialSwarmRelays.bind(this)) + return listener } /** @@ -121,4 +117,4 @@ class Dialer { } } -module.exports = Dialer +module.exports = Circuit diff --git a/src/circuit/constants.js b/src/circuit/constants.js deleted file mode 100644 index 6bb8301..0000000 --- a/src/circuit/constants.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict' - -module.exports = { - DIALER: { - ONION: 'onion', - TELESCOPE: 'telescope' - }, - RESPONSE: { - SUCCESS: 100, - HOP: { - SRC_ADDR_TOO_LONG: 220, - DST_ADDR_TOO_LONG: 221, - SRC_MULTIADDR_INVALID: 250, - DST_MULTIADDR_INVALID: 251, - NO_CONN_TO_DST: 260, - CANT_DIAL_DST: 261, - CANT_OPEN_DST_STREAM: 262, - CANT_SPEAK_RELAY: 270, - CANT_CONNECT_TO_SELF: 280, - MSG_TO_LONG: 290 - }, - STOP: { - SRC_ADDR_TOO_LONG: 320, - DST_ADDR_TOO_LONG: 321, - SRC_MULTIADDR_INVALID: 350, - DST_MULTIADDR_INVALID: 351, - MSG_TO_LONG: 350 - } - } -} diff --git a/src/circuit/dialer.js b/src/circuit/dialer.js index 0c965f8..ec158cf 100644 --- a/src/circuit/dialer.js +++ b/src/circuit/dialer.js @@ -34,14 +34,27 @@ class Dialer { * 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) { - throw new Error('abstract class, method not implemented') + dial (ma, cb) { + const addr = ma.toString().split('p2p-circuit') + const relay = addr[0] === '/' ? null : multiaddr(addr[0]) + const peer = multiaddr(addr[1] || addr[0]) + + const dstConn = new Connection() + setImmediate(this.dialPeer.bind(this), peer, relay, (err, conn) => { + if (err) { + log.err(err) + return cb(err) + } + dstConn.setInnerConn(conn) + cb(null, dstConn) + }) + + return dstConn } /** @@ -186,8 +199,22 @@ class Dialer { return cb(err) } - this.relayPeers.set(this.utils.getB58String(peer), peer) - cb(null) + streamHandler.read((err, msg) => { + if (err) { + log.err(err) + return cb(err) + } + + const response = proto.CircuitRelay.decode(msg) + + if (response.code !== proto.CircuitRelay.Status.SUCCESS) { + return log(`HOP not supported, skipping - ${this.utils.getB58String(peer)}`) + } + + log(`HOP supported adding as relay - ${this.utils.getB58String(peer)}`) + this.relayPeers.set(this.utils.getB58String(peer), peer) + cb(null) + }) }) }) } diff --git a/src/circuit/hop.js b/src/circuit/hop.js index 9164f37..75c5d14 100644 --- a/src/circuit/hop.js +++ b/src/circuit/hop.js @@ -8,7 +8,6 @@ const debug = require('debug') const PeerInfo = require('peer-info') const PeerId = require('peer-id') const EE = require('events').EventEmitter -const constants = require('./constants') const once = require('once') const utilsFactory = require('./utils') const StreamHandler = require('./stream-handler') @@ -97,7 +96,7 @@ class Hop extends EE { this._dialPeer(message.dstPeer, (err, dstConn) => { if (err) { const errStreamHandler = new StreamHandler(conn) - this._writeErr(errStreamHandler, constants.RESPONSE.CANT_DIAL_DST) + this.utils.writeResponse(errStreamHandler, proto.CircuitRelay.Status.HOP_CANT_DIAL_DST) pull(pull.empty(), errStreamHandler.rest()) log.err(err) return cb(err) @@ -119,7 +118,7 @@ class Hop extends EE { streamHandler.write(proto.CircuitRelay.encode(stopMsg), (err) => { if (err) { const errStreamHandler = new StreamHandler(conn) - this._writeErr(errStreamHandler, constants.RESPONSE.CANT_OPEN_DST_STREAM) + this.utils.writeResponse(errStreamHandler, proto.CircuitRelay.Status.HOP_CANT_OPEN_DST_STREAM) pull(pull.empty(), errStreamHandler.rest()) log.err(err) diff --git a/src/circuit/onion-dialer.js b/src/circuit/onion-dialer.js deleted file mode 100644 index 603b4d9..0000000 --- a/src/circuit/onion-dialer.js +++ /dev/null @@ -1,161 +0,0 @@ -'use strict' - -const setImmediate = require('async/setImmediate') - -const Dialer = require('./dialer') -const isFunction = require('lodash.isfunction') -const multiaddr = require('multiaddr') -const Connection = require('interface-connection').Connection -const multicodec = require('../multicodec') - -const debug = require('debug') -const log = debug('libp2p:circuit:oniondialer') -log.err = debug('libp2p:circuit:error:oniondialer') - -class OnionDialer extends Dialer { - /** - * 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 maddrs = multiaddr(ma).toString().split('/p2p-circuit').filter((a) => a.length) - if (maddrs.length > 0) { - const id = multiaddr(maddrs[maddrs.length - 1]).getPeerId() - if (this.swarm._peerInfo.id.toB58String() === id) { - let err = `cant dial to self!` - log.err(err) - return cb(err) - } - } - - let dstConn = new Connection() - setImmediate(this._onionDial.bind(this), maddrs, (err, conn) => { - if (err) { - log.err(err) - return cb(err) - } - dstConn.setInnerConn(conn) - cb(null, dstConn) - }) - - return dstConn - } - - /** - * Dial a peer using onion dial - dial all relays in the ma - * in sequence and circuit dest over the all the pipes - * - * @param {multiaddr} maddrs - * @param {Connection} relay - * @param {Function} cb - * @return {void} - * @private - */ - _onionDial (maddrs, relay, cb) { - if (isFunction(relay)) { - cb = relay - relay = null - } - - const dial = (dstAddr, relayPeer) => { - if (maddrs.length) { - this._createRelayPipe(dstAddr, relayPeer, (err, upgraded) => { - if (err) { - log.err(err) - return cb(err) - } - return this._onionDial(maddrs, upgraded, cb) - }) - } else { - this.dialPeer(dstAddr, relayPeer, (err, conn) => { - if (err) { - return cb(err) - } - cb(null, conn) - }) - } - } - - if (maddrs.length >= 2) { - const relayAddr = multiaddr(maddrs.shift()) - const destAddr = multiaddr(maddrs.shift()) - dial(destAddr, relayAddr) - } else { - dial(multiaddr(maddrs.shift()), relay) - } - } - - /** - * Creates a relay connection that can be used explicitly from two multiaddrs - * - * @param {Multiaddr} dstAddr - * @param {Multiaddr} relayPeer - * @param {Function} cb - * @returns {void|Function} - * @private - */ - _createRelayPipe (dstAddr, relayPeer, cb) { - this.dialPeer(dstAddr, relayPeer, (err, conn) => { - if (err) { - log.err(err) - return cb(err) - } - - dstAddr = multiaddr(dstAddr) - this.relayPeers.set(dstAddr.getPeerId(), conn) - this._handshake(this.utils.peerInfoFromMa(dstAddr), conn, (err, upgraded) => { - if (err) { - log.err(err) - return cb(err) - } - - cb(null, upgraded) - }) - }) - } - - /** - * Upgrade a raw connection to a relayed link (hop) - * - * @param {PeerInfo} pi - * @param {Connection} conn - * @param {Function} cb - * - * @return {Function|void} - * @private - */ - _handshake (pi, conn, cb) { - const proxyConn = new Connection() - const handler = this.swarm.connHandler(pi, multicodec.relay, proxyConn) - handler.handleNew(conn, (err, upgradedConn) => { - if (err) { - log.err(err) - return cb(err) - } - - if (!upgradedConn) { - proxyConn.setInnerConn(conn) - upgradedConn = proxyConn - } - - cb(null, upgradedConn) - }) - } -} - -module.exports = OnionDialer diff --git a/src/index.js b/src/index.js index 4f6f02b..e4c7fed 100644 --- a/src/index.js +++ b/src/index.js @@ -1,8 +1,3 @@ 'use strict' -module.exports = { - Hop: require('./circuit/hop'), - Dialer: require('./dialer'), - multicodec: require('./multicodec'), - tag: 'Circuit' -} +module.exports = require('./circuit') diff --git a/test/onion-dialer.spec.js b/test/onion-dialer.spec.js deleted file mode 100644 index 902277e..0000000 --- a/test/onion-dialer.spec.js +++ /dev/null @@ -1,172 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const Dialer = require('../src/circuit/onion-dialer') -const nodes = require('./fixtures/nodes') -const Connection = require('interface-connection').Connection -const multiaddr = require('multiaddr') -const PeerInfo = require('peer-info') -const PeerId = require('peer-id') -const waterfall = require('async/waterfall') - -const sinon = require('sinon') -const expect = require('chai').expect - -describe('onion dialer tests', function () { - describe('dial', function () { - const dialer = sinon.createStubInstance(Dialer) - - beforeEach(function (done) { - waterfall([ - (cb) => PeerId.createFromJSON(nodes.node4, cb), - (peerId, cb) => PeerInfo.create(peerId, cb), - (peer, cb) => { - dialer.swarm = { - _peerInfo: peer - } - cb() - } - ], done) - - dialer.dial.callThrough() - }) - - afterEach(function () { - dialer.dial.reset() - dialer._onionDial.reset() - }) - - it(`split the multiaddr correctly`, function (done) { - dialer._onionDial.callsArgWith(1, null, new Connection()) - - const chainedAddr = multiaddr( - `/ip4/0.0.0.0/tcp/9033/ws/ipfs/${nodes.node2.id}/p2p-circuit` + - `/ip4/0.0.0.0/tcp/9031/ipfs/${nodes.node1.id}/p2p-circuit/`) - - dialer.dial(chainedAddr + `/ipfs/${nodes.node3.id}`, (err, conn) => { - expect(err).to.be.null - expect(dialer._onionDial.calledOnce).to.be.ok - expect(dialer._onionDial.calledWith([ - `/ip4/0.0.0.0/tcp/9033/ws/ipfs/${nodes.node2.id}`, - `/ip4/0.0.0.0/tcp/9031/ipfs/${nodes.node1.id}`, - `/ipfs/${nodes.node3.id}` - ])).to.be.ok - done() - }) - }) - - it(`not dial to itself`, function (done) { - const chainedAddr = multiaddr( - `/ip4/0.0.0.0/tcp/9033/ws/ipfs/${nodes.node2.id}/p2p-circuit` + - `/ip4/0.0.0.0/tcp/9031/ipfs/${nodes.node1.id}/p2p-circuit/`) - - dialer.dial(chainedAddr + `/ipfs/${nodes.node4.id}`, (err, conn) => { - expect(err).to.not.be.null - expect(err).to.be.equal(`cant dial to self!`) - expect(dialer._onionDial.calledOnce).to.not.be.ok - done() - }) - }) - }) - - describe('_onionDial', function () { - describe('_onionDial chained address', function () { - const dialer = sinon.createStubInstance(Dialer) - dialer.relayPeers = new Map() - - let swarm = null - let upgraded = null - beforeEach(function (done) { - waterfall([ - (cb) => PeerId.createFromJSON(nodes.node4, cb), - (peerId, cb) => PeerInfo.create(peerId, cb), - (peer, cb) => { - swarm = { - _peerInfo: peer, - _peerBook: { - get: () => new PeerInfo(PeerId.createFromB58String('QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE')), - has: () => true - } - } - dialer.swarm = swarm - cb() - }, - (cb) => { - upgraded = new Connection() - dialer._onionDial.callThrough() - dialer.dialPeer.onCall(0).callsArgWith(2, null, new Connection()) - dialer._createRelayPipe.onCall(0).callsArgWith(2, null, upgraded) - dialer.dialPeer.onCall(1).callsArgWith(2, null, new Connection()) - cb() - } - ], done) - }) - - afterEach(function () { - dialer.dial.reset() - dialer.dialRelay.reset() - }) - - it(`dial chained multiaddr correctly`, function (done) { - const chainedAddrs = [ - `/ip4/0.0.0.0/tcp/9033/ws/ipfs/${nodes.node2.id}`, - `/ip4/0.0.0.0/tcp/9031/ipfs/${nodes.node1.id}`, - `/ipfs/${nodes.node3.id}` - ] - - const addr1 = multiaddr(chainedAddrs[0]) - const addr2 = multiaddr(chainedAddrs[1]) - const addr3 = multiaddr(chainedAddrs[2]) - - dialer._onionDial(chainedAddrs, (err, conn) => { - expect(err).to.be.null - expect(dialer._createRelayPipe.calledWith(addr2, addr1)).to.be.ok - expect(dialer.dialPeer.calledWith(addr3, upgraded)).to.be.ok - done() - }) - }) - }) - - describe('_onionDial non chained address', function () { - const dialer = sinon.createStubInstance(Dialer) - dialer.relayPeers = new Map() - - let swarm - beforeEach(function (done) { - waterfall([ - (cb) => PeerId.createFromJSON(nodes.node4, cb), - (peerId, cb) => PeerInfo.create(peerId, cb), - (peer, cb) => { - swarm = { - _peerInfo: peer, - _peerBook: { - get: () => new PeerInfo(PeerId.createFromB58String('QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE')), - has: () => true - } - } - dialer.swarm = swarm - cb() - }, - (cb) => { - dialer._onionDial.callThrough() - dialer.dialPeer.onCall(0).callsArgWith(2, null, new Connection()) - cb() - } - ], done) - }) - - afterEach(function () { - dialer.dial.reset() - dialer.dialRelay.reset() - }) - - it(`dial chained multiaddr correctly`, function (done) { - dialer._onionDial([`/ip4/0.0.0.0/tcp/9033/ws/ipfs/${nodes.node2.id}`], (err, conn) => { - expect(err).to.be.null - expect(dialer.dialPeer.calledWith(multiaddr(`/ip4/0.0.0.0/tcp/9033/ws/ipfs/${nodes.node2.id}`))).to.be.ok - done() - }) - }) - }) - }) -}) From f71892e573a3120e70107d4546ac6a40eef0fe2f Mon Sep 17 00:00:00 2001 From: Dmitriy Ryajov Date: Thu, 3 Aug 2017 12:26:11 -0700 Subject: [PATCH 19/20] 0.0.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e0a27ef..274233e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "libp2p-circuit", - "version": "0.0.2", + "version": "0.0.3", "description": "JavaScript implementation of circuit/switch relaying", "main": "src/index.js", "scripts": { From 24bd2fd3efe1b29e5496921d2a66996915a923da Mon Sep 17 00:00:00 2001 From: Dmitriy Ryajov Date: Wed, 9 Aug 2017 14:47:05 -0600 Subject: [PATCH 20/20] fix: go-js integration test changes --- src/circuit/dialer.js | 9 +++++---- src/circuit/hop.js | 12 ++++++++---- src/circuit/stop.js | 4 ++-- src/circuit/utils.js | 8 ++++---- src/listener.js | 2 +- src/protocol/proto.js | 27 ++++++++++++++------------- test/dialer.spec.js | 6 +++--- test/hop.spec.js | 25 +++++++++++++------------ test/listener.spec.js | 10 +++++----- 9 files changed, 55 insertions(+), 48 deletions(-) diff --git a/src/circuit/dialer.js b/src/circuit/dialer.js index ec158cf..54cb62c 100644 --- a/src/circuit/dialer.js +++ b/src/circuit/dialer.js @@ -7,6 +7,7 @@ const once = require('once') const waterfall = require('async/waterfall') const utilsFactory = require('./utils') const StreamHandler = require('./stream-handler') +const PeerId = require('peer-id') const debug = require('debug') const log = debug('libp2p:circuit:dialer') @@ -135,12 +136,12 @@ class Dialer { proto.CircuitRelay.encode({ type: proto.CircuitRelay.Type.HOP, srcPeer: { - id: this.swarm._peerInfo.id.toB58String(), - addrs: srcMas.map((addr) => addr.toString()) + id: this.swarm._peerInfo.id.id, + addrs: srcMas.map((addr) => addr.buffer) }, dstPeer: { - id: dstMa.getPeerId(), - addrs: [dstMa.toString()] + id: PeerId.createFromB58String(dstMa.getPeerId()).id, + addrs: [dstMa.buffer] } }), (err) => { diff --git a/src/circuit/hop.js b/src/circuit/hop.js index 75c5d14..0beb2a5 100644 --- a/src/circuit/hop.js +++ b/src/circuit/hop.js @@ -64,11 +64,15 @@ class Hop extends EE { return this.utils.writeResponse(streamHandler, proto.CircuitRelay.Status.SUCCESS) } - if (message.dstPeer.id === this.peerInfo.id.toB58String()) { + if (message.dstPeer.id.toString() === this.peerInfo.id.toB58String()) { return this.utils.writeResponse(streamHandler, proto.CircuitRelay.Status.HOP_CANT_RELAY_TO_SELF) } - this.utils.validateMsg(message, streamHandler, proto.CircuitRelay.Type.HOP, (err) => { + if (!message.dstPeer.addrs.length) { + message.dstPeer.addrs.push(`/ipfs/${message.dstPeer.id}`) + } + + this.utils.validateAddrs(message, streamHandler, proto.CircuitRelay.Type.HOP, (err) => { if (err) { return log(err) } @@ -148,8 +152,8 @@ class Hop extends EE { * @private */ _dialPeer (dstPeer, callback) { - const peerInfo = new PeerInfo(PeerId.createFromB58String(dstPeer.id)) - dstPeer.addrs.forEach((a) => peerInfo.multiaddrs.add(a.toString())) + const peerInfo = new PeerInfo(PeerId.createFromBytes(dstPeer.id)) + dstPeer.addrs.forEach((a) => peerInfo.multiaddrs.add(a)) this.swarm.dial(peerInfo, multicodec.relay, once((err, conn) => { if (err) { log.err(err) diff --git a/src/circuit/stop.js b/src/circuit/stop.js index a302471..c904e72 100644 --- a/src/circuit/stop.js +++ b/src/circuit/stop.js @@ -23,14 +23,14 @@ class Stop extends EE { handle (message, streamHandler, callback) { callback = callback || (() => {}) - return this.utils.validateMsg(message, streamHandler, proto.CircuitRelay.Type.STOP, (err) => { + return this.utils.validateAddrs(message, streamHandler, proto.CircuitRelay.Type.STOP, (err) => { if (err) { callback(err) return log(err) } const peerInfo = new PeerInfo(message.srcPeer.id) - message.srcPeer.addrs.forEach((addr) => peerInfo.multiaddrs.add(addr.toString())) + message.srcPeer.addrs.forEach((addr) => peerInfo.multiaddrs.add(addr)) const newConn = new Connection(streamHandler.rest()) newConn.setPeerInfo(peerInfo) setImmediate(() => this.emit('connection', newConn)) diff --git a/src/circuit/utils.js b/src/circuit/utils.js index 54b283f..7e54602 100644 --- a/src/circuit/utils.js +++ b/src/circuit/utils.js @@ -93,10 +93,10 @@ module.exports = function (swarm) { * @returns {*} * @param {Function} cb */ - function validateMsg (msg, streamHandler, type, cb) { + function validateAddrs (msg, streamHandler, type, cb) { try { msg.dstPeer.addrs.forEach((addr) => { - return multiaddr(addr.toString()) + return multiaddr(addr) }) } catch (err) { writeResponse(streamHandler, type === proto.CircuitRelay.Type.HOP @@ -107,7 +107,7 @@ module.exports = function (swarm) { try { msg.srcPeer.addrs.forEach((addr) => { - return multiaddr(addr.toString()) + return multiaddr(addr) }) } catch (err) { writeResponse(streamHandler, type === proto.CircuitRelay.Type.HOP @@ -123,7 +123,7 @@ module.exports = function (swarm) { getB58String: getB58String, peerInfoFromMa: peerInfoFromMa, isPeerConnected: isPeerConnected, - validateMsg: validateMsg, + validateAddrs: validateAddrs, writeResponse: writeResponse } } diff --git a/src/listener.js b/src/listener.js index 87d8a6e..4d9a5e0 100644 --- a/src/listener.js +++ b/src/listener.js @@ -40,7 +40,7 @@ module.exports = (swarm, options, connHandler) => { try { request = proto.CircuitRelay.decode(msg) } catch (err) { - return utils.writeResponse(streamHandler, proto.CircuitRelay.Status.INVALID_MSG_TYPE) + return utils.writeResponse(streamHandler, proto.CircuitRelay.Status.MALFORMED_MESSAGE) } switch (request.type) { diff --git a/src/protocol/proto.js b/src/protocol/proto.js index 4a9e29f..32a65f1 100644 --- a/src/protocol/proto.js +++ b/src/protocol/proto.js @@ -3,21 +3,22 @@ module.exports = ` message CircuitRelay { enum Status { - SUCCESS = 100; - INVALID_MSG_TYPE = 101; - HOP_SRC_ADDR_TOO_LONG= 220; - HOP_DST_ADDR_TOO_LONG= 221; - HOP_SRC_MULTIADDR_INVALID= 250; - HOP_DST_MULTIADDR_INVALID= 251; - HOP_NO_CONN_TO_DST = 260; - HOP_CANT_DIAL_DST = 261; - HOP_CANT_OPEN_DST_STREAM = 262; - HOP_CANT_SPEAK_RELAY = 270; - HOP_CANT_RELAY_TO_SELF = 280; - STOP_SRC_ADDR_TOO_LONG = 320; - STOP_DST_ADDR_TOO_LONG = 321; + SUCCESS = 100; + HOP_SRC_ADDR_TOO_LONG = 220; + HOP_DST_ADDR_TOO_LONG = 221; + HOP_SRC_MULTIADDR_INVALID = 250; + HOP_DST_MULTIADDR_INVALID = 251; + HOP_NO_CONN_TO_DST = 260; + HOP_CANT_DIAL_DST = 261; + HOP_CANT_OPEN_DST_STREAM = 262; + HOP_CANT_SPEAK_RELAY = 270; + HOP_CANT_RELAY_TO_SELF = 280; + STOP_SRC_ADDR_TOO_LONG = 320; + STOP_DST_ADDR_TOO_LONG = 321; STOP_SRC_MULTIADDR_INVALID = 350; STOP_DST_MULTIADDR_INVALID = 351; + STOP_RELAY_REFUSED = 390; + MALFORMED_MESSAGE = 400; } enum Type { // RPC identifier, either HOP, STOP or STATUS diff --git a/test/dialer.spec.js b/test/dialer.spec.js index 70ab44a..e8ba754 100644 --- a/test/dialer.spec.js +++ b/test/dialer.spec.js @@ -110,7 +110,7 @@ describe('dialer tests', function () { code: proto.CircuitRelay.Status.SUCCESS })) expect(err).to.be.null - expect(proto.CircuitRelay.decode(msg).dstPeer.addrs.toString()).to.be.equal(`${dstMa.toString()}`) + expect(proto.CircuitRelay.decode(msg).dstPeer.addrs[0]).to.deep.equal(dstMa.buffer) done() }) }) @@ -119,7 +119,7 @@ describe('dialer tests', function () { callback.callsFake((err, msg) => { expect(err).to.not.be.null expect(err).to.be.an.instanceOf(Error) - expect(err.message).to.be.equal(`Got 101 error code trying to dial over relay`) + expect(err.message).to.be.equal(`Got 400 error code trying to dial over relay`) expect(callback.calledOnce).to.be.ok done() }) @@ -130,7 +130,7 @@ describe('dialer tests', function () { pull( pull.values([proto.CircuitRelay.encode({ type: proto.CircuitRelay.Type.STATUS, - code: proto.CircuitRelay.Status.INVALID_MSG_TYPE + code: proto.CircuitRelay.Status.MALFORMED_MESSAGE })]), // send arbitrary non 200 code lp.encode(), pull.collect((err, encoded) => { diff --git a/test/hop.spec.js b/test/hop.spec.js index 5c07f69..7959cac 100644 --- a/test/hop.spec.js +++ b/test/hop.spec.js @@ -8,6 +8,7 @@ const handshake = require('pull-handshake') const waterfall = require('async/waterfall') const PeerInfo = require('peer-info') const PeerId = require('peer-id') +const multiaddr = require('multiaddr') const lp = require('pull-length-prefixed') const proto = require('../src/protocol') const StreamHandler = require('../src/circuit/stream-handler') @@ -59,12 +60,12 @@ describe('relay', function () { let relayMsg = { type: proto.CircuitRelay.Type.HOP, srcPeer: { - id: `QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`, - addrs: [`/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`] + id: Buffer.from(`QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`), + addrs: [multiaddr(`/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`).buffer] }, dstPeer: { - id: `QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`, - addrs: [`/ipfs/QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`] + id: Buffer.from(`QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`), + addrs: [multiaddr(`/ipfs/QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`).buffer] } } @@ -80,12 +81,12 @@ describe('relay', function () { let relayMsg = { type: proto.CircuitRelay.Type.HOP, srcPeer: { - id: `QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`, - addrs: [`/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`] + id: Buffer.from(`QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`), + addrs: [multiaddr(`/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`).buffer] }, dstPeer: { - id: `QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`, - addrs: [`/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`] + id: Buffer.from(`QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`), + addrs: [multiaddr(`/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`).buffer] } } @@ -109,8 +110,8 @@ describe('relay', function () { addrs: [`sdfkjsdnfkjdsb`] }, dstPeer: { - id: `QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`, - addrs: [`/ipfs/QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`] + id: Buffer.from(`QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`), + addrs: [multiaddr(`/ipfs/QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`).buffer] } } @@ -130,8 +131,8 @@ describe('relay', function () { let relayMsg = { type: proto.CircuitRelay.Type.HOP, srcPeer: { - id: `QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`, - addrs: [`/ipfs/QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`] + id: Buffer.from(`QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`), + addrs: [multiaddr(`/ipfs/QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`).buffer] }, dstPeer: { id: `sdfkjsdnfkjdsb`, diff --git a/test/listener.spec.js b/test/listener.spec.js index 084ecc6..18a6f29 100644 --- a/test/listener.spec.js +++ b/test/listener.spec.js @@ -173,12 +173,12 @@ describe('listener', function () { let relayMsg = { type: 100000, srcPeer: { - id: `QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`, - addrs: [`/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`] + id: Buffer.from(`QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`), + addrs: [multiaddr(`/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`).buffer] }, dstPeer: { - id: `QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`, - addrs: [`/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`] + id: Buffer.from(`QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`), + addrs: [multiaddr(`/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`).buffer] } } @@ -192,7 +192,7 @@ describe('listener', function () { lp.decodeFromReader(shake, {maxLength: this.maxLength}, (err, msg) => { expect(err).to.be.null expect(proto.CircuitRelay.decode(msg).type).to.equal(proto.CircuitRelay.Type.STATUS) - expect(proto.CircuitRelay.decode(msg).code).to.equal(proto.CircuitRelay.Status.INVALID_MSG_TYPE) + expect(proto.CircuitRelay.decode(msg).code).to.equal(proto.CircuitRelay.Status.MALFORMED_MESSAGE) done() }) )