From c47ab136685576f37d9f684691251a6a5be95418 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 5 Sep 2018 18:59:18 +0200 Subject: [PATCH 01/27] feat: add basic state machine functionality to switch --- package.json | 1 + src/dial.js | 2 +- src/index.js | 175 ++++++++++++++++++++++++++++++++++++--------------- 3 files changed, 128 insertions(+), 50 deletions(-) diff --git a/package.json b/package.json index 278cd53..b1b69b1 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "async": "^2.6.1", "big.js": "^5.1.2", "debug": "^3.1.0", + "fsm-event": "^2.1.0", "hashlru": "^2.2.1", "interface-connection": "~0.3.2", "ip-address": "^5.8.9", diff --git a/src/dial.js b/src/dial.js index 8f24e32..a98d7c3 100644 --- a/src/dial.js +++ b/src/dial.js @@ -461,7 +461,7 @@ class Dialer { /** * Returns a Dialer generator that when called, will immediately begin dialing - * fo the given `peer`. + * to the given `peer`. * * @param {Switch} _switch * @returns {function(PeerInfo, string, function(Error, Connection))} diff --git a/src/index.js b/src/index.js index a3db99d..efb76df 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,7 @@ -'use strict' +'use strict'; -const EE = require('events').EventEmitter +const FSM = require('fsm-event') +const EventEmitter = require('events').EventEmitter const each = require('async/each') const series = require('async/series') const TransportManager = require('./transport') @@ -13,8 +14,10 @@ const Observer = require('./observer') const Stats = require('./stats') const assert = require('assert') const Errors = require('./errors') +const debug = require('debug') +const log = debug('libp2p:switch') -class Switch extends EE { +class Switch extends EventEmitter { constructor (peerInfo, peerBook, options) { super() assert(peerInfo, 'You must provide a `peerInfo`') @@ -70,6 +73,40 @@ class Switch extends EE { // higher level (public) API this.dial = dial(this) + + // Setup the internal state + this.state = new FSM('STOPPED', { + STOPPED: { + start: 'STARTING', + stop: 'STOPPING' // ensures that any transports that were manually started are stopped + }, + STARTING: { + done: 'STARTED', + stop: 'STOPPING' + }, + STARTED: { + stop: 'STOPPING', + start: 'STARTED' + }, + STOPPING: { done: 'STOPPED' } + }) + this.state.on('STARTING', () => { + log('The switch is starting') + this._onStarting() + }) + this.state.on('STOPPING', () => { + log('The switch is stopping') + this._onStopping() + }) + this.state.on('STARTED', () => { + log('The switch has started') + this.emit('started') + }) + this.state.on('STOPPED', () => { + log('The switch has stopped') + this.emit('stopped') + }) + this.state.on('error', (err) => this.emit('error', err)) } /** @@ -90,52 +127,6 @@ class Switch extends EE { }) } - /** - * Starts the Switch listening on all available Transports - * - * @param {function(Error)} callback - * @returns {void} - */ - start (callback) { - each(this.availableTransports(this._peerInfo), (ts, cb) => { - // Listen on the given transport - this.transport.listen(ts, {}, null, cb) - }, callback) - } - - /** - * Stops all services and connections for the Switch - * - * @param {function(Error)} callback - * @returns {void} - */ - stop (callback) { - this.stats.stop() - series([ - (cb) => each(this.muxedConns, (conn, cb) => { - // If the connection was destroyed while we are hanging up, continue - if (!conn) { - return cb() - } - - conn.muxer.end((err) => { - // If OK things are fine, and someone just shut down - if (err && err.message !== 'Fatal error: OK') { - return cb(err) - } - cb() - }) - }, cb), - (cb) => { - each(this.transports, (transport, cb) => { - each(transport.listeners, (listener, cb) => { - listener.close(cb) - }, cb) - }, cb) - } - ], callback) - } - /** * Adds the `handlerFunc` and `matchFunc` to the Switch's protocol * handler list for the given `protocol`. If the `matchFunc` returns @@ -197,6 +188,92 @@ class Switch extends EE { const transports = Object.keys(this.transports).filter((t) => t !== 'Circuit') return transports && transports.length > 0 } + + /** + * Issues a start on the Switch state. + * + * @param {function} callback deprecated: Listening for the `error` and `start` events are recommended + * @fires Switch#started + * @returns {void} + */ + start (callback = () => {}) { + // Add once listener for deprecated callback support + this.once('started', callback) + + this.state('start') + } + + /** + * Issues a stop on the Switch state. + * + * @param {function} callback deprecated: Listening for the `error` and `stop` events are recommended + * @fires Switch#stop + * @returns {void} + */ + stop (callback = () => {}) { + // Add once listener for deprecated callback support + this.once('stopped', callback) + + this.state('stop') + } + + /** + * A listener that will start any necessary services and listeners + * + * @fires Switch#error + * @returns {void} + */ + _onStarting () { + each(this.availableTransports(this._peerInfo), (ts, cb) => { + // Listen on the given transport + this.transport.listen(ts, {}, null, cb) + }, (err) => { + if (err) { + return this.emit('error', err) + } + this.state('done') + }) + } + + /** + * A listener that will turn off all running services and listeners + * + * @fires Switch#error + * @returns {void} + */ + _onStopping () { + this.stats.stop() + series([ + (cb) => each(this.muxedConns, (conn, cb) => { + // If the connection was destroyed while we are hanging up, continue + if (!conn) { + return cb() + } + + conn.muxer.end((err) => { + // If OK things are fine, and someone just shut down + if (err && err.message !== 'Fatal error: OK') { + return cb(err) + } + cb() + }) + }, cb), + (cb) => { + each(this.transports, (transport, cb) => { + each(transport.listeners, (listener, cb) => { + listener.close(cb) + }, cb) + }, cb) + } + ], (err) => { + if (err) { + console.log('Error', err) + this.emit('error', err) + } + + this.state('done') + }) + } } module.exports = Switch From a97c3863ddcad3226cdfdab0bc27156fe3ad47b6 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 5 Sep 2018 19:01:52 +0200 Subject: [PATCH 02/27] fix: linting --- src/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index efb76df..ece145e 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict' const FSM = require('fsm-event') const EventEmitter = require('events').EventEmitter @@ -17,6 +17,11 @@ const Errors = require('./errors') const debug = require('debug') const log = debug('libp2p:switch') +/** + * @fires Switch#stopped Triggered when the switch has stopped + * @fires Switch#started Triggered when the switch has started + * @fires Switch#error Triggered whenever an error occurs + */ class Switch extends EventEmitter { constructor (peerInfo, peerBook, options) { super() From b4d160217fe396638527eea221d02e79813e8218 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 5 Sep 2018 19:12:21 +0200 Subject: [PATCH 03/27] refactor: move connection.js to connection-manager.js --- src/{connection.js => connection-manager.js} | 0 src/index.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/{connection.js => connection-manager.js} (100%) diff --git a/src/connection.js b/src/connection-manager.js similarity index 100% rename from src/connection.js rename to src/connection-manager.js diff --git a/src/index.js b/src/index.js index ece145e..aaa51c1 100644 --- a/src/index.js +++ b/src/index.js @@ -5,7 +5,7 @@ const EventEmitter = require('events').EventEmitter const each = require('async/each') const series = require('async/series') const TransportManager = require('./transport') -const ConnectionManager = require('./connection') +const ConnectionManager = require('./connection-manager') const getPeerInfo = require('./get-peer-info') const dial = require('./dial') const ProtocolMuxer = require('./protocol-muxer') From 41e4a88f70e100d3f8270996ef5f003559ac8369 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Tue, 11 Sep 2018 13:35:27 +0200 Subject: [PATCH 04/27] feat: add outgoing connection state machine --- Connections.md | 35 +++ package.json | 1 + src/connection-manager.js | 2 +- src/connection.js | 510 ++++++++++++++++++++++++++++++++++++ src/dial.js | 524 ++++++------------------------------- src/dial.old.js | 495 +++++++++++++++++++++++++++++++++++ src/errors.js | 12 + src/index.js | 11 +- src/limit-dialer/index.js | 2 +- src/limit-dialer/queue.js | 10 +- src/transport.js | 21 ++ test/circuit-relay.node.js | 35 ++- test/connection.node.js | 266 +++++++++++++++++++ test/node.js | 1 + 14 files changed, 1450 insertions(+), 475 deletions(-) create mode 100644 Connections.md create mode 100644 src/connection.js create mode 100644 src/dial.old.js create mode 100644 test/connection.node.js diff --git a/Connections.md b/Connections.md new file mode 100644 index 0000000..a88984f --- /dev/null +++ b/Connections.md @@ -0,0 +1,35 @@ +# Connections + +Libp2p Switch creates stateful connections between peers. This enables connections to be more easily reused and upgraded. + +## Lifecycle + +### Base Connection +* When no connection exists between peers, a new base connection is created + * Once established the base connection will be privatized, if needed + * Once privatized the connection will be encrypted + +### Muxed Connection +* If a muxer exists on the switch, the base connection will attempt to upgrade + * If the upgrade fails and their is no protocol, upgrading stops and the base connection is saved but not yet used + * If the upgrade fails and their is a protocol, upgrading stops and the base connection is used + * If the upgrade works, the upgraded connnection is used + * Future dial requests will use this connection. + * Future protocol negotiation will use spawned streams from this connection. + +### Protocol Handshaking +* If a protocol was provided on the dial request, handshaking will occur + * If the connection was upgraded (muxed), a new stream is created for the handshake + * If the connection was not upgraded, the current connection is used for the handshake + * If the handshake is successful, the resulting connection will be passed back via the dial calls callback. + + + +No Connection -> .dial -> basic connection *base_connection* +Basic connection -> .protect -> private connection *private* + or Basic Connection -> .encrypt -> encrypted connection *encrypted* +Private Connection -> .encrypt -> encrypted connection *encrypted* +Encrypted Connection -> .upgrade -> upgraded connection *upgraded* + or Encrypted Connection -> .upgrade -> +Encrypted Connection -> .shake(protocol) -> Connected _cannot reuse_ *connection* +Upgraded Connection -> .shake(protocol) -> new stream _upraded conn can be used_ *stream* diff --git a/package.json b/package.json index b1b69b1..52cd420 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "async": "^2.6.1", "big.js": "^5.1.2", "debug": "^3.1.0", + "err-code": "^1.1.2", "fsm-event": "^2.1.0", "hashlru": "^2.2.1", "interface-connection": "~0.3.2", diff --git a/src/connection-manager.js b/src/connection-manager.js index 395bae6..1773eb8 100644 --- a/src/connection-manager.js +++ b/src/connection-manager.js @@ -4,7 +4,7 @@ const identify = require('libp2p-identify') const multistream = require('multistream-select') const waterfall = require('async/waterfall') const debug = require('debug') -const log = debug('libp2p:switch:connection') +const log = debug('libp2p:switch:conn-manager') const once = require('once') const setImmediate = require('async/setImmediate') diff --git a/src/connection.js b/src/connection.js new file mode 100644 index 0000000..4ecd6dd --- /dev/null +++ b/src/connection.js @@ -0,0 +1,510 @@ +'use strict' + +const FSM = require('fsm-event') +const EventEmitter = require('events').EventEmitter +const setImmediate = require('async/setImmediate') +const Circuit = require('libp2p-circuit') +const multistream = require('multistream-select') + +const observeConnection = require('./observe-connection') +const Errors = require('./errors') +const debug = require('debug') + +/** + * @typedef {Object} ConnectionOptions + * @property {Switch} _switch Our switch instance + * @property {PeerInfo} peerInfo The PeerInfo of the peer to dial + * @property {Muxer} muxer Optional - A muxed connection + */ + +/** + * ConnectionFSM handles the complex logic of managing a connection + * between peers. ConnectionFSM is internally composed of a state machine + * to help improve the usability and debuggability of connections. The + * state machine also helps to improve the ability to handle dial backoff, + * coalescing dials and dial locks. + */ +class ConnectionFSM extends EventEmitter { + /** + * Determines if the given connection is an instance of ConnectionFSM + * + * @static + * @param {*} connection + * @returns {boolean} + */ + static isConnection(connection) { + return connection instanceof ConnectionFSM + } + + /** + * @param {ConnectionOptions} param0 + * @constructor + */ + constructor ({ _switch, peerInfo, muxer }) { + super() + + this.switch = _switch + this.theirPeerInfo = peerInfo + this.theirB58Id = this.theirPeerInfo.id.toB58String() + this.ourPeerInfo = this.switch._peerInfo + this.log = debug(`libp2p:switch:connection:${this.ourPeerInfo.id.toB58String().slice(0, 8)}`) + + this.conn = null // The base connection + this.muxer = muxer // The upgraded/muxed connection + + // TODO: If given a muxer, we need to set the state + // at connected. + // * A muxed connection should be fully connected. + // * A protocol handshake should generate a new connection + + this._state = FSM('DISCONNECTED', { + DISCONNECTED: { // No active connections exist for the peer + dial: 'DIALING' + }, + DIALING: { // Creating an initial connection + abort: 'ABORTED', + // emit events for different transport dials? + done: 'DIALED', + error: 'ERRORED', + disconnect: 'DISCONNECTING' + }, + DIALED: { // Base connection to peer established + encrypt: 'ENCRYPTING', + privatize: 'PRIVATIZING' + }, + PRIVATIZING: { // Protecting the base connection + done: 'PRIVATIZED', + abort: 'ABORTED', + disconnect: 'DISCONNECTING' + }, + PRIVATIZED: { // Base connection is protected + encrypt: 'ENCRYPTING' + }, + ENCRYPTING: { // Encrypting the base connection + done: 'ENCRYPTED', + error: 'ERRORED', + disconnect: 'DISCONNECTING' + }, + ENCRYPTED: { // Upgrading could not happen, the connection is encrypted and waiting + upgrade: 'UPGRADING', + disconnect: 'DISCONNECTING' + }, + UPGRADING: { // Attempting to upgrade the connection with muxers + stop: 'CONNECTED', // If we cannot mux, stop upgrading + done: 'MUXED', + error: 'ERRORED' + }, + MUXED: { + disconnect: 'DISCONNECTING' + }, + CONNECTED: { // A non muxed connection is established + disconnect: 'DISCONNECTING' + }, + DISCONNECTING: { // Shutting down the connection + done: 'DISCONNECTED' + }, + ABORTED: { }, // A severe event occurred + ERRORED: { // An error occurred, but future dials may be allowed + disconnect: 'DISCONNECTING' // There could be multiple options here, but this is a likely action + } + }) + + this._state.on('DISCONNECTED', () => this._onDisconnected()) + this._state.on('DIALING', () => this._onDialing()) + this._state.on('DIALED', () => this._onDialed()) + this._state.on('PRIVATIZING', () => this._onPrivatizing()) + this._state.on('PRIVATIZED', () => { + this.log(`successfully privatized conn to ${this.theirB58Id}`) + this.emit('private', this.conn) + }) + this._state.on('ENCRYPTING', () => this._onEncrypting()) + this._state.on('ENCRYPTED', () => { + this.log(`successfully encrypted connection to ${this.theirB58Id}`) + this.emit('encrypted', this.conn) + }) + this._state.on('UPGRADING', () => this._onUpgrading()) + this._state.on('MUXED', () => { + this.log(`successfully muxed connection to ${this.theirB58Id}`) + this.emit('muxed', this.muxer) + }) + this._state.on('CONNECTED', () => { + this.log(`unmuxed connection opened to ${this.theirB58Id}`) + this.emit('unmuxed', this.conn) + }) + this._state.on('DISCONNECTING', () => this._onDisconnecting()) + this._state.on('ABORTED', () => this._onAborted()) + this._state.on('ERRORED', () => this._onErrored()) + this._state.on('error', (err) => this._onStateError(err)) + } + + /** + * Gets the current state of the connection + * + * @returns {string} The current state of the connection + */ + getState () { + return this._state._state + } + + /** + * Puts the state into dialing mode + * + * @fires ConnectionFSM#Error May emit a DIAL_SELF error + * @returns {void} + */ + dial () { + if (this.theirB58Id === this.ourPeerInfo.id.toB58String()) { + return this.emit('error', Errors.DIAL_SELF()) + } + + this._state('dial') + } + + /** + * Puts the state into encrypting mode + * + * @returns {void} + */ + encrypt () { + this._state('encrypt') + } + + /** + * Puts the state into privatizing mode + * + * @returns {void} + */ + protect () { + this._state('privatize') + } + + /** + * Initiates a handshake for the given protocol + * + * @param {string} protocol The protocol to negotiate + * @param {function(Error, Connection)} callback + * @returns {void} + */ + shake (protocol, callback) { + // If there is no protocol set yet, don't perform the handshake + if (!protocol) { + return callback(null, null) + } + + if (this.muxer && this.muxer.newStream) { + return this.muxer.newStream((err, stream) => { + if (err) { + return callback(err, null) + } + + this.log(`created new stream to ${this.theirB58Id}`) + this._protocolHandshake(protocol, stream, callback) + }) + } + + this.conn.setPeerInfo(this.theirPeerInfo) + this._protocolHandshake(protocol, this.conn, callback) + } + + /** + * Puts the state into muxing mode + * + * @returns {void} + */ + upgrade () { + this._state('upgrade') + } + + /** + * Event handler for dialing. Transitions state when successful. + * + * @private + * @fires ConnectionFSM#error + * @returns {void} + */ + _onDialing () { + // TODO: Start the connection flow + // TODO: Allow multiple dials? + this.log(`dialing ${this.theirB58Id}`) + + if (!this.switch.hasTransports()) { + this.emit('error', Errors.NO_TRANSPORTS_REGISTERED()) + return this._state('disconnect') + } + + const tKeys = this.switch.availableTransports(this.theirPeerInfo) + + const circuitEnabled = Boolean(this.switch.transports[Circuit.tag]) + let circuitTried = false + + const nextTransport = (key) => { + let transport = key + if (!transport) { + if (!circuitEnabled) { + this.emit('error', new Error( + `Circuit not enabled and all transports failed to dial peer ${this.theirB58Id}!` + )) + return this._state('disconnect') + } + + if (circuitTried) { + this.emit('error', new Error(`No available transports to dial peer ${this.theirB58Id}!`)) + return this._state('disconnect') + } + + this.log(`Falling back to dialing over circuit`) + this.theirPeerInfo.multiaddrs.add(`/p2p-circuit/ipfs/${this.theirB58Id}`) + circuitTried = true + transport = Circuit.tag + } + + this.log(`dialing transport ${transport}`) + this.switch.transport.dial(transport, this.theirPeerInfo, (err, _conn) => { + if (err) { + this.log(err) + return nextTransport(tKeys.shift()) + } + + this.conn = observeConnection(transport, null, _conn, this.switch.observer) + this._state('done') + }) + } + + nextTransport(tKeys.shift()) + } + + /** + * Once a connection has been successfully dialed, the connection + * will be privatized or encrypted depending on the presence of the + * Switch.protector. + * + * @returns {void} + */ + _onDialed () { + this.log(`successfully dialed ${this.theirB58Id}`) + + this.emit('connected', this.conn) + } + + /** + * Event handler for disconneced. + * + * @returns {void} + */ + _onDisconnected () { + this.log(`disconnected from ${this.theirB58Id}`) + } + + /** + * Event handler for disconnecting. Handles any needed cleanup + * + * @returns {void} + */ + _onDisconnecting () { + this.log(`disconnecting from ${this.theirB58Id}`) + + // Issue disconnects on both Peers + if (this.theirPeerInfo) { + this.theirPeerInfo.disconnect() + this.log(`closed connection to ${this.theirB58Id}`) + } + // TODO: should we do this? + if (this.ourPeerInfo) { + this.ourPeerInfo.disconnect() + } + + // Clean up stored connections + if (this.muxer) { + setImmediate(() => this.switch.emit('peer-mux-closed', this.theirPeerInfo)) + } + delete this.switch.muxedConns[this.theirB58Id] + delete this.switch.conns[this.theirB58Id] + delete this.muxer + delete this.conn + + this._state('done') + } + + /** + * Wraps this.conn with the Switch.protector for private connections + * + * @private + * @fires ConnectionFSM#error + * @returns {void} + */ + _onPrivatizing () { + this.conn = this.switch.protector.protect(this.conn, (err) => { + if (err) { + this.emit('error', err) + return this._state('disconnect') + } + + this.conn.setPeerInfo(this.theirPeerInfo) + this._state('done') + }) + } + + /** + * Attempts to encrypt `this.conn` with the Switch's crypto. + * + * @private + * @fires ConnectionFSM#error + * @returns {void} + */ + _onEncrypting () { + const msDialer = new multistream.Dialer() + msDialer.handle(this.conn, (err) => { + if (err) { + this.emit('error', Errors.maybeUnexpectedEnd(err)) + return this._state('disconnect') + } + + this.log('selecting crypto %s to %s', this.switch.crypto.tag, this.theirB58Id) + + msDialer.select(this.switch.crypto.tag, (err, _conn) => { + if (err) { + this.emit('error', Errors.maybeUnexpectedEnd(err)) + return this._state('disconnect') + } + + const conn = observeConnection(null, this.switch.crypto.tag, _conn, this.switch.observer) + + this.conn = this.switch.crypto.encrypt(this.ourPeerInfo.id, conn, this.theirPeerInfo.id, (err) => { + if (err) { + this.emit('error', err) + return this._state('disconnect') + } + + this.conn.setPeerInfo(this.theirPeerInfo) + this._state('done') + }) + }) + }) + } + + /** + * Iterates over each Muxer on the Switch and attempts to upgrade + * the given `connection`. Successful muxed connections will be stored + * on the Switch.muxedConns with `b58Id` as their key for future reference. + * + * @private + * @returns {void} + */ + _onUpgrading () { + const muxers = Object.keys(this.switch.muxers) + this.log(`upgrading connection to ${this.theirB58Id}`) + + if (muxers.length === 0) { + return this._state('stop') + } + + const msDialer = new multistream.Dialer() + msDialer.handle(this.conn, (err) => { + if (err) { + return this._didUpgrade(err) + } + + // 1. try to handshake in one of the muxers available + // 2. if succeeds + // - add the muxedConn to the list of muxedConns + // - add incomming new streams to connHandler + const nextMuxer = (key) => { + this.log('selecting %s', key) + msDialer.select(key, (err, _conn) => { + if (err) { + if (muxers.length === 0) { + return this._didUpgrade(err) + } + + return nextMuxer(muxers.shift()) + } + + // observe muxed connections + const conn = observeConnection(null, key, _conn, this.switch.observer) + + this.muxer = this.switch.muxers[key].dialer(conn) + this.switch.muxedConns[this.theirB58Id] = this + + this.muxer.once('close', () => { + this._state('disconnect') + }) + + // For incoming streams, in case identify is on + this.muxer.on('stream', (conn) => { + this.log(`new stream created via muxer to ${this.theirB58Id}`) + conn.setPeerInfo(this.theirPeerInfo) + this.switch.protocolMuxer(null)(conn) + }) + + setImmediate(() => this.switch.emit('peer-mux-established', this.theirPeerInfo)) + + this._didUpgrade(null) + }) + } + + nextMuxer(muxers.shift()) + }) + } + + /** + * Analyses the given error, if it exists, to determine where the state machine + * needs to go. + * + * @param {Error} err + */ + _didUpgrade (err) { + if (err) { + this.log('Error upgrading connection:', err) + this.switch.conns[this.theirB58Id] = this + // Cant upgrade, hold the encrypted connection + return this._state('stop') + } + + // move the state machine forward + this._state('done') + } + + /** + * Performs the protocol handshake for the given protocol + * over the given connection. The resulting error or connection + * will be returned via the callback. + * + * @private + * @param {string} protocol + * @param {Connection} connection + * @param {function(Error, Connection)} callback + * @returns {void} + */ + _protocolHandshake (protocol, connection, callback) { + const msDialer = new multistream.Dialer() + msDialer.handle(connection, (err) => { + if (err) { + return callback(err, null) + } + + msDialer.select(protocol, (err, _conn) => { + if (err) { + this.log(`could not perform protocol handshake: `, err) + return callback(err, null) + } + + const conn = observeConnection(null, protocol, _conn, this.switch.observer) + this.log(`successfully performed handshake of ${protocol} to ${this.theirB58Id}`) + callback(null, conn) + }) + }) + } + + /** + * Event handler for state transition errors + * + * @param {Error} err + * @returns {void} + */ + _onStateError (err) { + // TODO: may need to do something for legit invalid transitions + this.log(err) + } +} + +module.exports = ConnectionFSM diff --git a/src/dial.js b/src/dial.js index a98d7c3..acefea7 100644 --- a/src/dial.js +++ b/src/dial.js @@ -1,462 +1,28 @@ 'use strict' -const multistream = require('multistream-select') const Connection = require('interface-connection').Connection +const ConnectionFSM = require('./connection') +const getPeerInfo = require('./get-peer-info') +const once = require('once') const setImmediate = require('async/setImmediate') -const Circuit = require('libp2p-circuit') -const waterfall = require('async/waterfall') const debug = require('debug') const log = debug('libp2p:switch:dial') -const getPeerInfo = require('./get-peer-info') -const observeConnection = require('./observe-connection') -const UNEXPECTED_END = 'Unexpected end of input from reader.' - -/** - * Uses the given MultistreamDialer to select the protocol matching the given key - * - * A helper method to catch errors from pull streams ending unexpectedly - * Needed until https://github.com/dignifiedquire/pull-length-prefixed/pull/8 is merged. - * - * @param {MultistreamDialer} msDialer a multistream.Dialer - * @param {string} key The key type to select - * @param {function(Error)} callback Used for standard async flow - * @param {function(Error)} abort A callback to be used for ending the connection outright - * @returns {void} - */ -function selectSafe (msDialer, key, callback, abort) { - msDialer.select(key, (err, conn) => { - if (err === true) { - return abort(new Error(UNEXPECTED_END)) - } - - callback(err, conn) - }) -} - -/** - * Uses the given MultistreamDialer to handle the given connection - * - * A helper method to catch errors from pull streams ending unexpectedly - * Needed until https://github.com/dignifiedquire/pull-length-prefixed/pull/8 is merged - * - * @param {MultistreamDialer} msDialer - * @param {Connection} connection The connection to handle - * @param {function(Error)} callback Used for standard async flow - * @param {function(Error)} abort A callback to be used for ending the connection outright - * @returns {void} - */ -function handleSafe (msDialer, connection, callback, abort) { - msDialer.handle(connection, (err) => { - // Repackage errors from pull-streams ending unexpectedly. - // Needed until https://github.com/dignifiedquire/pull-length-prefixed/pull/8 is merged. - if (err === true) { - return abort(new Error(UNEXPECTED_END)) - } - - callback(err) - }) -} - -/** - * Manages dialing to another peer, including muxer upgrades - * and crypto management. The main entry point for dialing is - * Dialer.dial - * - * @param {Switch} _switch - * @param {PeerInfo} peerInfo - * @param {string} protocol - * @param {function(Error, Connection)} callback - */ -class Dialer { - constructor (_switch, peerInfo, ourPeerInfo, protocol, callback) { - this.switch = _switch - this.peerInfo = peerInfo - this.ourPeerInfo = ourPeerInfo - this.protocol = protocol - this.callback = callback - } - - /** - * Initializes a proxy connection and returns it. The connection is also immediately - * dialed. This will include establishing the base connection, crypto, muxing and the - * protocol handshake if all needed components have already been set. - * - * @returns {Connection} - */ - dial () { - const proxyConnection = new Connection() - proxyConnection.setPeerInfo(this.peerInfo) - - waterfall([ - (cb) => { - this._establishConnection(cb) - }, - (connection, cb) => { - if (connection) { - proxyConnection.setPeerInfo(this.peerInfo) - proxyConnection.setInnerConn(connection) - return cb(null, proxyConnection) - } - cb(null) - } - ], (err, connection) => { - if ((err && err.message === UNEXPECTED_END) || err === true) { - log('Connection dropped for %s', this.peerInfo.id.toB58String()) - return this.callback(null, null) - } - - this.callback(err, connection) - }) - - return proxyConnection - } - - /** - * Establishes a base connection and then continues to upgrade that connection - * including: crypto, muxing and the protocol handshake. If any upgrade is not - * yet available, or already exists, the upgrade will continue where it left off. - * - * @private - * @param {function(Error, Connection)} callback - * @returns {void} - */ - _establishConnection (callback) { - const b58Id = this.peerInfo.id.toB58String() - log('dialing %s', b58Id) - if (b58Id === this.ourPeerInfo.id.toB58String()) { - return callback(new Error('A node cannot dial itself')) - } - - waterfall([ - (cb) => { - // Start with a base connection, which includes encryption - this._createBaseConnection(b58Id, cb) - }, - (baseConnection, cb) => { - // Upgrade the connection with a muxer - this._createMuxedConnection(baseConnection, b58Id, cb) - }, - (muxer, cb) => { - // If we have no protocol, dont continue with the handshake - if (!this.protocol) { - return cb() - } - - // If we have a muxer, create a new stream, otherwise it's a standard connection - if (muxer.newStream) { - muxer.newStream((err, conn) => { - if (err) return cb(err) - - this._performProtocolHandshake(conn, cb) - }) - return - } - - this._performProtocolHandshake(muxer, cb) - } - ], (err, connection) => { - callback(err, connection) - }) - } - - /** - * If the base connection already exists to the PeerId key, `b58Id`, - * it will be returned in the callback. If no connection exists, one will - * be attempted via Dialer.attemptDial. - * - * @private - * @param {string} b58Id - * @param {function(Error, Connection)} callback - * @returns {void} - */ - _createBaseConnection (b58Id, callback) { - const baseConnection = this.switch.conns[b58Id] - const muxedConnection = this.switch.muxedConns[b58Id] - - // if the muxed connection exists, dont return a connection, - // _createMuxedConnection will get the connection - if (muxedConnection) { - return callback(null, null) - } - if (baseConnection) { - this.switch.conns[b58Id] = undefined - return callback(null, baseConnection) - } - - waterfall([ - (cb) => { - this._attemptDial(cb) - }, - (baseConnection, cb) => { - // Create a private connection if it's needed - this._createPrivateConnection(baseConnection, cb) - }, - (connection, cb) => { - // Add the Switch's crypt encryption to the connection - this._encryptConnection(connection, cb) - } - ], (err, encryptedConnection) => { - if (err) { +function maybePerformHandshake ({ protocol, proxyConnection, connection, callback }) { + if (protocol) { + return connection.shake(protocol, (err, conn) => { + if (!conn) { return callback(err) } - callback(null, encryptedConnection) + proxyConnection.setPeerInfo(connection.theirPeerInfo) + proxyConnection.setInnerConn(conn) + callback(null, proxyConnection) }) } - /** - * If the switch has a private network protector, `switch.protector`, its `protect` - * method will be called with the given connection. The resulting, wrapped connection - * will be returned via the callback. - * - * @param {Connection} connection The connection to protect - * @param {function(Error, Connection)} callback - * @returns {void} - */ - _createPrivateConnection (connection, callback) { - if (this.switch.protector === null) { - return callback(null, connection) - } - - // If the switch has a protector, be private - const protectedConnection = this.switch.protector.protect(connection, (err) => { - if (err) { - return callback(err) - } - - protectedConnection.setPeerInfo(this.peerInfo) - callback(null, protectedConnection) - }) - } - - /** - * If the given PeerId key, `b58Id`, has an existing muxed connection - * it will be returned via the callback, otherwise the connection - * upgrade will be initiated via Dialer.attemptMuxerUpgrade. - * - * @private - * @param {Connection} connection - * @param {string} b58Id - * @param {function(Error, Connection)} callback - * @returns {void} - */ - _createMuxedConnection (connection, b58Id, callback) { - const muxedConnection = this.switch.muxedConns[b58Id] - if (muxedConnection) { - return callback(null, muxedConnection.muxer) - } - - connection.setPeerInfo(this.peerInfo) - this._attemptMuxerUpgrade(connection, b58Id, (err, muxer) => { - if (err && !this.protocol) { - this.switch.conns[b58Id] = connection - return callback(null, null) - } - - if (err) { - log('muxer upgrade failed with error', err) - // couldn't upgrade to Muxer, it is ok, use the existing connection - return callback(null, connection) - } - - callback(null, muxer) - }, callback) - } - - /** - * Iterates over each Muxer on the Switch and attempts to upgrade - * the given `connection`. Successful muxed connections will be stored - * on the Switch.muxedConns with `b58Id` as their key for future reference. - * - * @private - * @param {Connection} connection - * @param {string} b58Id - * @param {function(Error, Connection)} callback - * @param {function(Error, Connection)} abort A callback to be used for ending the connection outright - * @returns {void} - */ - _attemptMuxerUpgrade (connection, b58Id, callback, abort) { - const muxers = Object.keys(this.switch.muxers) - - if (muxers.length === 0) { - return callback(new Error('no muxers available')) - } - - const msDialer = new multistream.Dialer() - handleSafe(msDialer, connection, (err) => { - if (err) { - return callback(new Error('multistream not supported')) - } - - // 1. try to handshake in one of the muxers available - // 2. if succeeds - // - add the muxedConn to the list of muxedConns - // - add incomming new streams to connHandler - const nextMuxer = (key) => { - log('selecting %s', key) - selectSafe(msDialer, key, (err, _conn) => { - if (err) { - if (muxers.length === 0) { - return callback(new Error('could not upgrade to stream muxing')) - } - - return nextMuxer(muxers.shift()) - } - - // observe muxed connections - const conn = observeConnection(null, key, _conn, this.switch.observer) - - const muxedConn = this.switch.muxers[key].dialer(conn) - this.switch.muxedConns[b58Id] = { - muxer: muxedConn - } - - muxedConn.once('close', () => { - delete this.switch.muxedConns[b58Id] - this.peerInfo.disconnect() - this.switch._peerInfo.disconnect() - log(`closed connection to ${b58Id}`) - setImmediate(() => this.switch.emit('peer-mux-closed', this.peerInfo)) - }) - - // For incoming streams, in case identify is on - muxedConn.on('stream', (conn) => { - conn.setPeerInfo(this.peerInfo) - this.switch.protocolMuxer(null)(conn) - }) - - setImmediate(() => this.switch.emit('peer-mux-established', this.peerInfo)) - - callback(null, muxedConn) - }, abort) - } - - nextMuxer(muxers.shift()) - }, abort) - } - - /** - * Iterates over each Transport on the Switch and attempts to connect - * to the peer. Once a Transport succeeds, no additional Transports will - * be dialed. - * - * @private - * @param {function(Error, Connection)} callback - * @returns {void} - */ - _attemptDial (callback) { - if (!this.switch.hasTransports()) { - return callback(new Error('No transports registered, dial not possible')) - } - - const tKeys = this.switch.availableTransports(this.peerInfo) - - const circuitEnabled = Boolean(this.switch.transports[Circuit.tag]) - let circuitTried = false - - const nextTransport = (key) => { - let transport = key - const b58Id = this.peerInfo.id.toB58String() - if (!transport) { - if (!circuitEnabled) { - const msg = `Circuit not enabled and all transports failed to dial peer ${b58Id}!` - return callback(new Error(msg)) - } - - if (circuitTried) { - return callback(new Error(`No available transports to dial peer ${b58Id}!`)) - } - - log(`Falling back to dialing over circuit`) - this.peerInfo.multiaddrs.add(`/p2p-circuit/ipfs/${b58Id}`) - circuitTried = true - transport = Circuit.tag - } - - log(`dialing transport ${transport}`) - this.switch.transport.dial(transport, this.peerInfo, (err, _conn) => { - if (err) { - log(err) - return nextTransport(tKeys.shift()) - } - - const conn = observeConnection(transport, null, _conn, this.switch.observer) - callback(null, conn) - }) - } - - nextTransport(tKeys.shift()) - } - - /** - * Attempts to encrypt the given `connection` with the Switch's crypto. - * - * @private - * @param {Connection} connection - * @param {function(Error, Connection)} callback - * @returns {void} - */ - _encryptConnection (connection, callback) { - const msDialer = new multistream.Dialer() - handleSafe(msDialer, connection, (err) => { - if (err) { - return callback(err) - } - - const myId = this.switch._peerInfo.id - log('selecting crypto: %s', this.switch.crypto.tag) - - selectSafe(msDialer, this.switch.crypto.tag, (err, _conn) => { - if (err) { - return callback(err) - } - - const conn = observeConnection(null, this.switch.crypto.tag, _conn, this.switch.observer) - - const encryptedConnection = this.switch.crypto.encrypt(myId, conn, this.peerInfo.id, (err) => { - if (err) { - return callback(err) - } - - encryptedConnection.setPeerInfo(this.peerInfo) - callback(null, encryptedConnection) - }) - }, callback) - }, callback) - } - - /** - * Initiates a handshake for the Dialer's set protocol - * - * @private - * @param {Connection} connection - * @param {function(Error, Connection)} callback - * @returns {void} - */ - _performProtocolHandshake (connection, callback) { - // If there is no protocol set yet, don't perform the handshake - if (!this.protocol) { - callback() - } - - const msDialer = new multistream.Dialer() - handleSafe(msDialer, connection, (err) => { - if (err) { - return callback(err) - } - - selectSafe(msDialer, this.protocol, (err, _conn) => { - if (err) { - log(`could not perform protocol handshake: `, err) - return callback(err) - } - const conn = observeConnection(null, this.protocol, _conn, this.switch.observer) - callback(null, conn) - }, callback) - }, callback) - } + callback() } /** @@ -481,12 +47,74 @@ function dial (_switch) { protocol = null } - callback = callback || function noop () {} + callback = once(callback || function noop () {}) const peerInfo = getPeerInfo(peer, _switch._peerBook) - const dialer = new Dialer(_switch, peerInfo, _switch._peerInfo, protocol, callback) + const b58Id = peerInfo.id.toB58String() - return dialer.dial() + log(`${_switch._peerInfo.id.toB58String().slice(0,8)} dial request to ${b58Id.slice(0,8)} with protocol ${protocol}`) + + let connection = _switch.muxedConns[b58Id] || _switch.conns[b58Id] + + // TODO: make the listen logic of switch create an fsm + if (!ConnectionFSM.isConnection(connection)) { + connection = new ConnectionFSM({ + _switch, + peerInfo, + muxer: _switch.muxedConns[b58Id] || null + }) + } + + // TODO: Add listeners to the connection to control its state + + // TODO: Add logic here for handling previous connections + const proxyConnection = new Connection() + proxyConnection.setPeerInfo(peerInfo) + + connection.once('error', (err) => { + // console.log('got an err', err) + callback(err) + }) + connection.once('connected', () => { + if (_switch.protector) { + return connection.protect() + } + connection.encrypt() + }) + connection.once('private', () => connection.encrypt()) + connection.once('encrypted', () => connection.upgrade()) + connection.once('muxed', () => { + maybePerformHandshake({ + protocol, + proxyConnection, + connection, + callback + }) + }) + connection.once('unmuxed', () => { + maybePerformHandshake({ + protocol, + proxyConnection, + connection, + callback + }) + }) + + // If we have a connection, maybe perform the protocol handshake + // TODO: The basic connection probably shouldnt be reused + const state = connection.getState() + if (state === 'CONNECTED' || state === 'MUXED') { + maybePerformHandshake({ + protocol, + proxyConnection, + connection, + callback + }) + } else { + connection.dial() + } + + return proxyConnection } } diff --git a/src/dial.old.js b/src/dial.old.js new file mode 100644 index 0000000..d1fbe81 --- /dev/null +++ b/src/dial.old.js @@ -0,0 +1,495 @@ +'use strict' + +const multistream = require('multistream-select') +const Connection = require('interface-connection').Connection +const setImmediate = require('async/setImmediate') +const Circuit = require('libp2p-circuit') +const waterfall = require('async/waterfall') + +const debug = require('debug') +const log = debug('libp2p:switch:dial') + +const getPeerInfo = require('./get-peer-info') +const observeConnection = require('./observe-connection') +const UNEXPECTED_END = 'Unexpected end of input from reader.' + +/** + * Uses the given MultistreamDialer to select the protocol matching the given key + * + * A helper method to catch errors from pull streams ending unexpectedly + * Needed until https://github.com/dignifiedquire/pull-length-prefixed/pull/8 is merged. + * + * @param {MultistreamDialer} msDialer a multistream.Dialer + * @param {string} key The key type to select + * @param {function(Error)} callback Used for standard async flow + * @param {function(Error)} abort A callback to be used for ending the connection outright + * @returns {void} + */ +function selectSafe (msDialer, key, callback, abort) { + msDialer.select(key, (err, conn) => { + if (err === true) { + return abort(new Error(UNEXPECTED_END)) + } + + callback(err, conn) + }) +} + +/** + * Uses the given MultistreamDialer to handle the given connection + * + * A helper method to catch errors from pull streams ending unexpectedly + * Needed until https://github.com/dignifiedquire/pull-length-prefixed/pull/8 is merged + * + * @param {MultistreamDialer} msDialer + * @param {Connection} connection The connection to handle + * @param {function(Error)} callback Used for standard async flow + * @param {function(Error)} abort A callback to be used for ending the connection outright + * @returns {void} + */ +function handleSafe (msDialer, connection, callback, abort) { + msDialer.handle(connection, (err) => { + // Repackage errors from pull-streams ending unexpectedly. + // Needed until https://github.com/dignifiedquire/pull-length-prefixed/pull/8 is merged. + if (err === true) { + return abort(new Error(UNEXPECTED_END)) + } + + callback(err) + }) +} + +/** + * Manages dialing to another peer, including muxer upgrades + * and crypto management. The main entry point for dialing is + * Dialer.dial + * + * @param {Switch} _switch + * @param {PeerInfo} peerInfo + * @param {string} protocol + * @param {function(Error, Connection)} callback + */ +class Dialer { + constructor (_switch, peerInfo, ourPeerInfo, protocol, callback) { + this.switch = _switch + this.peerInfo = peerInfo + this.ourPeerInfo = ourPeerInfo + this.protocol = protocol + this.callback = callback + } + + /** + * Initializes a proxy connection and returns it. The connection is also immediately + * dialed. This will include establishing the base connection, crypto, muxing and the + * protocol handshake if all needed components have already been set. + * + * @returns {Connection} + */ + dial () { + const proxyConnection = new Connection() + proxyConnection.setPeerInfo(this.peerInfo) + + waterfall([ + (cb) => { + this._establishConnection(cb) + }, + (connection, cb) => { + if (connection) { + proxyConnection.setPeerInfo(this.peerInfo) + proxyConnection.setInnerConn(connection) + return cb(null, proxyConnection) + } + cb(null) + } + ], (err, connection) => { + if ((err && err.message === UNEXPECTED_END) || err === true) { + log('Connection dropped for %s', this.peerInfo.id.toB58String()) + return this.callback(null, null) + } + + this.callback(err, connection) + }) + + return proxyConnection + } + + /** + * Establishes a base connection and then continues to upgrade that connection + * including: crypto, muxing and the protocol handshake. If any upgrade is not + * yet available, or already exists, the upgrade will continue where it left off. + * + * @private + * @param {function(Error, Connection)} callback + * @returns {void} + */ + _establishConnection (callback) { + const b58Id = this.peerInfo.id.toB58String() + log('dialing %s', b58Id) + if (b58Id === this.ourPeerInfo.id.toB58String()) { + return callback(new Error('A node cannot dial itself')) + } + + waterfall([ + (cb) => { + // Start with a base connection, which includes encryption + this._createBaseConnection(b58Id, cb) + }, + (baseConnection, cb) => { + // Upgrade the connection with a muxer + this._createMuxedConnection(baseConnection, b58Id, cb) + }, + (muxer, cb) => { + // If we have no protocol, dont continue with the handshake + if (!this.protocol) { + return cb() + } + + // If we have a muxer, create a new stream, otherwise it's a standard connection + if (muxer.newStream) { + muxer.newStream((err, conn) => { + if (err) return cb(err) + + this._performProtocolHandshake(conn, cb) + }) + return + } + + this._performProtocolHandshake(muxer, cb) + } + ], (err, connection) => { + callback(err, connection) + }) + } + + /** + * If the base connection already exists to the PeerId key, `b58Id`, + * it will be returned in the callback. If no connection exists, one will + * be attempted via Dialer.attemptDial. + * + * @private + * @param {string} b58Id + * @param {function(Error, Connection)} callback + * @returns {void} + */ + _createBaseConnection (b58Id, callback) { + const baseConnection = this.switch.conns[b58Id] + const muxedConnection = this.switch.muxedConns[b58Id] + + // if the muxed connection exists, dont return a connection, + // _createMuxedConnection will get the connection + if (muxedConnection) { + return callback(null, null) + } + if (baseConnection) { + this.switch.conns[b58Id] = undefined + return callback(null, baseConnection) + } + + waterfall([ + (cb) => { + this._attemptDial(cb) + }, + (baseConnection, cb) => { + // Create a private connection if it's needed + this._createPrivateConnection(baseConnection, cb) + }, + (connection, cb) => { + // Add the Switch's crypt encryption to the connection + this._encryptConnection(connection, cb) + } + ], (err, encryptedConnection) => { + if (err) { + return callback(err) + } + + callback(null, encryptedConnection) + }) + } + + /** + * If the switch has a private network protector, `switch.protector`, its `protect` + * method will be called with the given connection. The resulting, wrapped connection + * will be returned via the callback. + * + * @param {Connection} connection The connection to protect + * @param {function(Error, Connection)} callback + * @returns {void} + */ + _createPrivateConnection (connection, callback) { + if (this.switch.protector === null) { + return callback(null, connection) + } + + // If the switch has a protector, be private + const protectedConnection = this.switch.protector.protect(connection, (err) => { + if (err) { + return callback(err) + } + + protectedConnection.setPeerInfo(this.peerInfo) + callback(null, protectedConnection) + }) + } + + /** + * If the given PeerId key, `b58Id`, has an existing muxed connection + * it will be returned via the callback, otherwise the connection + * upgrade will be initiated via Dialer.attemptMuxerUpgrade. + * + * @private + * @param {Connection} connection + * @param {string} b58Id + * @param {function(Error, Connection)} callback + * @returns {void} + */ + _createMuxedConnection (connection, b58Id, callback) { + const muxedConnection = this.switch.muxedConns[b58Id] + if (muxedConnection) { + return callback(null, muxedConnection.muxer) + } + + connection.setPeerInfo(this.peerInfo) + this._attemptMuxerUpgrade(connection, b58Id, (err, muxer) => { + if (err && !this.protocol) { + this.switch.conns[b58Id] = connection + return callback(null, null) + } + + if (err) { + log('muxer upgrade failed with error', err) + // couldn't upgrade to Muxer, it is ok, use the existing connection + return callback(null, connection) + } + + callback(null, muxer) + }, callback) + } + + /** + * Iterates over each Muxer on the Switch and attempts to upgrade + * the given `connection`. Successful muxed connections will be stored + * on the Switch.muxedConns with `b58Id` as their key for future reference. + * + * @private + * @param {Connection} connection + * @param {string} b58Id + * @param {function(Error, Connection)} callback + * @param {function(Error, Connection)} abort A callback to be used for ending the connection outright + * @returns {void} + */ + _attemptMuxerUpgrade (connection, b58Id, callback, abort) { + const muxers = Object.keys(this.switch.muxers) + + if (muxers.length === 0) { + return callback(new Error('no muxers available')) + } + + const msDialer = new multistream.Dialer() + handleSafe(msDialer, connection, (err) => { + if (err) { + return callback(new Error('multistream not supported')) + } + + // 1. try to handshake in one of the muxers available + // 2. if succeeds + // - add the muxedConn to the list of muxedConns + // - add incomming new streams to connHandler + const nextMuxer = (key) => { + log('selecting %s', key) + selectSafe(msDialer, key, (err, _conn) => { + if (err) { + if (muxers.length === 0) { + return callback(new Error('could not upgrade to stream muxing')) + } + + return nextMuxer(muxers.shift()) + } + + // observe muxed connections + const conn = observeConnection(null, key, _conn, this.switch.observer) + + const muxedConn = this.switch.muxers[key].dialer(conn) + this.switch.muxedConns[b58Id] = { + muxer: muxedConn + } + + muxedConn.once('close', () => { + delete this.switch.muxedConns[b58Id] + this.peerInfo.disconnect() + this.switch._peerInfo.disconnect() + log(`closed connection to ${b58Id}`) + setImmediate(() => this.switch.emit('peer-mux-closed', this.peerInfo)) + }) + + // For incoming streams, in case identify is on + muxedConn.on('stream', (conn) => { + conn.setPeerInfo(this.peerInfo) + this.switch.protocolMuxer(null)(conn) + }) + + setImmediate(() => this.switch.emit('peer-mux-established', this.peerInfo)) + + callback(null, muxedConn) + }, abort) + } + + nextMuxer(muxers.shift()) + }, abort) + } + + /** + * Iterates over each Transport on the Switch and attempts to connect + * to the peer. Once a Transport succeeds, no additional Transports will + * be dialed. + * + * @private + * @param {function(Error, Connection)} callback + * @returns {void} + */ + _attemptDial (callback) { + if (!this.switch.hasTransports()) { + return callback(new Error('No transports registered, dial not possible')) + } + + const tKeys = this.switch.availableTransports(this.peerInfo) + + const circuitEnabled = Boolean(this.switch.transports[Circuit.tag]) + let circuitTried = false + + const nextTransport = (key) => { + let transport = key + const b58Id = this.peerInfo.id.toB58String() + if (!transport) { + if (!circuitEnabled) { + const msg = `Circuit not enabled and all transports failed to dial peer ${b58Id}!` + return callback(new Error(msg)) + } + + if (circuitTried) { + return callback(new Error(`No available transports to dial peer ${b58Id}!`)) + } + + log(`Falling back to dialing over circuit`) + this.peerInfo.multiaddrs.add(`/p2p-circuit/ipfs/${b58Id}`) + circuitTried = true + transport = Circuit.tag + } + + log(`dialing transport ${transport}`) + this.switch.transport.dial(transport, this.peerInfo, (err, _conn) => { + if (err) { + log(err) + return nextTransport(tKeys.shift()) + } + + const conn = observeConnection(transport, null, _conn, this.switch.observer) + callback(null, conn) + }) + } + + nextTransport(tKeys.shift()) + } + + /** + * Attempts to encrypt the given `connection` with the Switch's crypto. + * + * @private + * @param {Connection} connection + * @param {function(Error, Connection)} callback + * @returns {void} + */ + _encryptConnection (connection, callback) { + const msDialer = new multistream.Dialer() + handleSafe(msDialer, connection, (err) => { + if (err) { + return callback(err) + } + + const myId = this.switch._peerInfo.id + log('selecting crypto: %s', this.switch.crypto.tag) + + selectSafe(msDialer, this.switch.crypto.tag, (err, _conn) => { + if (err) { + return callback(err) + } + + const conn = observeConnection(null, this.switch.crypto.tag, _conn, this.switch.observer) + + const encryptedConnection = this.switch.crypto.encrypt(myId, conn, this.peerInfo.id, (err) => { + if (err) { + return callback(err) + } + + encryptedConnection.setPeerInfo(this.peerInfo) + callback(null, encryptedConnection) + }) + }, callback) + }, callback) + } + + /** + * Initiates a handshake for the Dialer's set protocol + * + * @private + * @param {Connection} connection + * @param {function(Error, Connection)} callback + * @returns {void} + */ + _performProtocolHandshake (connection, callback) { + // If there is no protocol set yet, don't perform the handshake + if (!this.protocol) { + callback() + } + + console.log(connection.peerInfo, connection.info.peerInfo) + + const msDialer = new multistream.Dialer() + handleSafe(msDialer, connection, (err) => { + if (err) { + return callback(err) + } + + selectSafe(msDialer, this.protocol, (err, _conn) => { + if (err) { + log(`could not perform protocol handshake: `, err) + return callback(err) + } + const conn = observeConnection(null, this.protocol, _conn, this.switch.observer) + callback(null, conn) + }, callback) + }, callback) + } +} + +/** + * Returns a Dialer generator that when called, will immediately begin dialing + * to the given `peer`. + * + * @param {Switch} _switch + * @returns {function(PeerInfo, string, function(Error, Connection))} + */ +function dial (_switch) { + /** + * Creates a new dialer and immediately begins dialing to the given `peer` + * + * @param {PeerInfo} peer + * @param {string} protocol + * @param {function(Error, Connection)} callback + * @returns {Connection} + */ + return (peer, protocol, callback) => { + if (typeof protocol === 'function') { + callback = protocol + protocol = null + } + + callback = callback || function noop () {} + + const peerInfo = getPeerInfo(peer, _switch._peerBook) + const dialer = new Dialer(_switch, peerInfo, _switch._peerInfo, protocol, callback) + + return dialer.dial() + } +} + +module.exports = dial diff --git a/src/errors.js b/src/errors.js index bbc1193..192a23f 100644 --- a/src/errors.js +++ b/src/errors.js @@ -1,3 +1,15 @@ 'use strict' +const errCode = require('err-code') + module.exports.PROTECTOR_REQUIRED = 'No protector provided with private network enforced' +module.exports.DIAL_SELF = () => errCode(new Error('A node cannot dial itself'), 'DIAL_SELF') +module.exports.NO_TRANSPORTS_REGISTERED = () => errCode(new Error('No transports registered, dial not possible'), 'NO_TRANSPORTS_REGISTERED') +module.exports.UNEXPECTED_END = () => errCode(new Error('Unexpected end of input from reader.'), 'UNEXPECTED_END') + +module.exports.maybeUnexpectedEnd = (err) => { + if (err === true) { + return module.exports.UNEXPECTED_END() + } + return err +} diff --git a/src/index.js b/src/index.js index aaa51c1..c3b0aaf 100644 --- a/src/index.js +++ b/src/index.js @@ -163,7 +163,7 @@ class Switch extends EventEmitter { /** * If a muxed Connection exists for the given peer, it will be closed - * and its reference on the Switch will be removed. + * and its reference on the Switch and will be removed. * * @param {PeerInfo|Multiaddr|PeerId} peer * @param {function()} callback @@ -225,6 +225,7 @@ class Switch extends EventEmitter { /** * A listener that will start any necessary services and listeners * + * @private * @fires Switch#error * @returns {void} */ @@ -243,6 +244,7 @@ class Switch extends EventEmitter { /** * A listener that will turn off all running services and listeners * + * @private * @fires Switch#error * @returns {void} */ @@ -270,12 +272,7 @@ class Switch extends EventEmitter { }, cb) }, cb) } - ], (err) => { - if (err) { - console.log('Error', err) - this.emit('error', err) - } - + ], (_) => { this.state('done') }) } diff --git a/src/limit-dialer/index.js b/src/limit-dialer/index.js index 33f423f..7d0208c 100644 --- a/src/limit-dialer/index.js +++ b/src/limit-dialer/index.js @@ -4,7 +4,7 @@ const map = require('async/map') const debug = require('debug') const once = require('once') -const log = debug('libp2p:swarm:dialer') +const log = debug('libp2p:switch:dialer') const DialQueue = require('./queue') diff --git a/src/limit-dialer/queue.js b/src/limit-dialer/queue.js index df23d1f..c883589 100644 --- a/src/limit-dialer/queue.js +++ b/src/limit-dialer/queue.js @@ -6,7 +6,7 @@ const timeout = require('async/timeout') const queue = require('async/queue') const debug = require('debug') -const log = debug('libp2p:swarm:dialer:queue') +const log = debug('libp2p:switch:dialer:queue') /** * Queue up the amount of dials to a given peer. @@ -37,15 +37,15 @@ class DialQueue { * @private */ _doWork (transport, addr, token, callback) { - log('work') + log(`${transport.constructor.name}:work:start`) this._dialWithTimeout(transport, addr, (err, conn) => { if (err) { - log('work:error') + log(`${transport.constructor.name}:work:error`, err) return callback(null, {error: err}) } if (token.cancel) { - log('work:cancel') + log(`${transport.constructor.name}:work:cancel`) // clean up already done dials pull(pull.empty(), conn) // TODO: proper cleanup once the connection interface supports it @@ -56,7 +56,7 @@ class DialQueue { // one is enough token.cancel = true - log('work:success') + log(`${transport.constructor.name}:work:success`) const proxyConn = new Connection() proxyConn.setInnerConn(conn) diff --git a/src/transport.js b/src/transport.js index 86ebb87..9a9c211 100644 --- a/src/transport.js +++ b/src/transport.js @@ -43,6 +43,27 @@ class TransportManager { } } + /** + * Closes connections for the given transport key + * and removes it from the switch. + * + * @param {String} key + * @param {function(Error)} callback + * @returns {void} + */ + remove (key, callback) { + callback = callback || function() {} + + if (!this.switch.transports[key]) { + return callback() + } + + this.close(key, (err) => { + delete this.switch.transports[key] + callback(err) + }) + } + /** * For a given transport `key`, dial to all that transport multiaddrs * diff --git a/test/circuit-relay.node.js b/test/circuit-relay.node.js index 04bc2e7..fddc3e5 100644 --- a/test/circuit-relay.node.js +++ b/test/circuit-relay.node.js @@ -19,6 +19,7 @@ const getPorts = require('portfinder').getPorts const utils = require('./utils') const createInfos = utils.createInfos const Swarm = require('../src') +const debug = require('debug') describe(`circuit`, function () { let swarmA // TCP and WS @@ -271,9 +272,11 @@ describe(`circuit`, function () { }) it('should be able to dial tcp -> tcp', (done) => { - tcpSwitch2.once('peer-mux-established', (peerInfo) => { - expect(peerInfo.id.toB58String()).to.equal(tcpPeer1.id.toB58String()) - done() + tcpSwitch2.on('peer-mux-established', function handle (peerInfo) { + if (peerInfo.id.toB58String() === tcpPeer1.id.toB58String()) { + tcpSwitch2.off('peer-mux-established', handle) + done() + } }) tcpSwitch1.dial(tcpPeer2, (err, connection) => { expect(err).to.not.exist() @@ -283,9 +286,11 @@ describe(`circuit`, function () { }) it('should be able to dial tcp -> ws over relay', (done) => { - wsSwitch1.once('peer-mux-established', (peerInfo) => { - expect(peerInfo.id.toB58String()).to.equal(tcpPeer1.id.toB58String()) - done() + wsSwitch1.on('peer-mux-established', function handle (peerInfo) { + if (peerInfo.id.toB58String() === tcpPeer1.id.toB58String()) { + wsSwitch1.off('peer-mux-established', handle) + done() + } }) tcpSwitch1.dial(wsPeer1, (err, connection) => { expect(err).to.not.exist() @@ -295,9 +300,11 @@ describe(`circuit`, function () { }) it('should be able to dial ws -> ws', (done) => { - wsSwitch2.once('peer-mux-established', (peerInfo) => { - expect(peerInfo.id.toB58String()).to.equal(wsPeer1.id.toB58String()) - done() + wsSwitch2.on('peer-mux-established', function handle (peerInfo) { + if (peerInfo.id.toB58String() === wsPeer1.id.toB58String()) { + wsSwitch2.off('peer-mux-established', handle) + done() + } }) wsSwitch1.dial(wsPeer2, (err, connection) => { expect(err).to.not.exist() @@ -307,10 +314,12 @@ describe(`circuit`, function () { }) it('should be able to dial ws -> tcp over relay', (done) => { - tcpSwitch1.once('peer-mux-established', (peerInfo) => { - expect(peerInfo.id.toB58String()).to.equal(wsPeer2.id.toB58String()) - expect(Object.keys(tcpSwitch1._peerBook.getAll())).to.include(wsPeer2.id.toB58String()) - done() + tcpSwitch1.on('peer-mux-established', function handle (peerInfo) { + if (peerInfo.id.toB58String() === wsPeer2.id.toB58String()) { + tcpSwitch1.off('peer-mux-established', handle) + expect(Object.keys(tcpSwitch1._peerBook.getAll())).to.include(wsPeer2.id.toB58String()) + done() + } }) wsSwitch2.dial(tcpPeer1, (err, connection) => { expect(err).to.not.exist() diff --git a/test/connection.node.js b/test/connection.node.js new file mode 100644 index 0000000..425cf9b --- /dev/null +++ b/test/connection.node.js @@ -0,0 +1,266 @@ +/* eslint-env mocha */ +'use strict' + +const chai = require('chai') +const dirtyChai = require('dirty-chai') +const expect = chai.expect +chai.use(dirtyChai) +const sinon = require('sinon') +const PeerBook = require('peer-book') +const WS = require('libp2p-websockets') +const parallel = require('async/parallel') +const secio = require('libp2p-secio') +const pull = require('pull-stream') +const multiplex = require('libp2p-mplex') +const Connection = require('interface-connection').Connection +const Protector = require('libp2p-pnet') +const generatePSK = Protector.generate + +const psk = Buffer.alloc(95) +generatePSK(psk) + +const ConnectionFSM = require('../src/connection') +const Switch = require('../src') +const Errors = require('../src/errors') +const createInfos = require('./utils').createInfos +const tryEcho = require('./utils').tryEcho + +describe('ConnectionFSM', () => { + let listenerSwitch + let dialerSwitch + + before((done) => { + createInfos(2, (err, infos) => { + if (err) { + return done(err) + } + + dialerSwitch = new Switch(infos.shift(), new PeerBook()) + dialerSwitch._peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/15451/ws') + dialerSwitch.connection.crypto(secio.tag, secio.encrypt) + dialerSwitch.connection.addStreamMuxer(multiplex) + dialerSwitch.transport.add('ws', new WS()) + + listenerSwitch = new Switch(infos.shift(), new PeerBook()) + listenerSwitch._peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/15452/ws') + listenerSwitch.connection.crypto(secio.tag, secio.encrypt) + listenerSwitch.connection.addStreamMuxer(multiplex) + listenerSwitch.transport.add('ws', new WS()) + + parallel([ + (cb) => dialerSwitch.start(cb), + (cb) => listenerSwitch.start(cb) + ], (err) => { + done(err) + }) + }) + }) + + after((done) => { + parallel([ + (cb) => dialerSwitch.stop(cb), + (cb) => listenerSwitch.stop(cb) + ], () => { + done() + }) + }) + + it('should have a default state of disconnected', () => { + const connection = new ConnectionFSM({ + _switch: dialerSwitch, + peerInfo: listenerSwitch._peerInfo + }) + + expect(connection.getState()).to.equal('DISCONNECTED') + }) + + it('.dial should create a basic connection', (done) => { + const connection = new ConnectionFSM({ + _switch: dialerSwitch, + peerInfo: listenerSwitch._peerInfo + }) + + connection.once('connected', (conn) => { + expect(conn).to.be.an.instanceof(Connection) + done() + }) + + connection.dial() + }) + + it('should be able to encrypt a basic connection', (done) => { + const connection = new ConnectionFSM({ + _switch: dialerSwitch, + peerInfo: listenerSwitch._peerInfo + }) + + connection.once('connected', (conn) => { + expect(conn).to.be.an.instanceof(Connection) + connection.encrypt() + }) + connection.once('encrypted', (conn) => { + expect(conn).to.be.an.instanceof(Connection) + done() + }) + + connection.dial() + }) + + it('should be able to upgrade an encrypted connection', (done) => { + const connection = new ConnectionFSM({ + _switch: dialerSwitch, + peerInfo: listenerSwitch._peerInfo + }) + + connection.once('connected', (conn) => { + expect(conn).to.be.an.instanceof(Connection) + connection.encrypt() + }) + connection.once('encrypted', (conn) => { + expect(conn).to.be.an.instanceof(Connection) + connection.upgrade() + }) + connection.once('muxed', (conn) => { + expect(conn.multicodec).to.equal(multiplex.multicodec) + done() + }) + + connection.dial() + }) + + it('should be able to handshake a protocol over a muxed connection', (done) => { + const connection = new ConnectionFSM({ + _switch: dialerSwitch, + peerInfo: listenerSwitch._peerInfo + }) + + listenerSwitch.handle('/muxed-conn-test/1.0.0', (_, conn) => { + return pull(conn, conn) + }) + + connection.once('connected', (conn) => { + expect(conn).to.be.an.instanceof(Connection) + connection.encrypt() + }) + connection.once('encrypted', (conn) => { + expect(conn).to.be.an.instanceof(Connection) + connection.upgrade() + }) + connection.once('muxed', (conn) => { + expect(conn.multicodec).to.equal(multiplex.multicodec) + + connection.shake('/muxed-conn-test/1.0.0', (err, protocolConn) => { + expect(err).to.not.exist() + expect(protocolConn).to.be.an.instanceof(Connection) + done() + }) + }) + + connection.dial() + }) + + describe('with no muxers', () => { + let oldMuxers + before(() => { + oldMuxers = dialerSwitch.muxers + dialerSwitch.muxers = {} + }) + + after(() => { + dialerSwitch.muxers = oldMuxers + }) + + it('should be able to handshake a protocol over a basic connection', (done) => { + const connection = new ConnectionFSM({ + _switch: dialerSwitch, + peerInfo: listenerSwitch._peerInfo + }) + + listenerSwitch.handle('/unmuxed-conn-test/1.0.0', (_, conn) => { + return pull(conn, conn) + }) + + connection.once('connected', (conn) => { + expect(conn).to.be.an.instanceof(Connection) + connection.encrypt() + }) + connection.once('encrypted', (conn) => { + expect(conn).to.be.an.instanceof(Connection) + connection.upgrade() + }) + connection.once('muxed', () => { + throw new Error('connection shouldnt be muxed') + }) + connection.once('unmuxed', (conn) => { + expect(conn).to.be.an.instanceof(Connection) + + connection.shake('/unmuxed-conn-test/1.0.0', (err, protocolConn) => { + expect(err).to.not.exist() + expect(protocolConn).to.be.an.instanceof(Connection) + done() + }) + }) + + connection.dial() + }) + }) + + describe('with a protector', () => { + // Restart the switches with protectors + before((done) => { + parallel([ + (cb) => dialerSwitch.stop(cb), + (cb) => listenerSwitch.stop(cb) + ], () => { + dialerSwitch.protector = new Protector(psk) + listenerSwitch.protector = new Protector(psk) + + parallel([ + (cb) => dialerSwitch.start(cb), + (cb) => listenerSwitch.start(cb) + ], done) + }) + }) + + it('should be able to protect a basic connection', (done) => { + const connection = new ConnectionFSM({ + _switch: dialerSwitch, + peerInfo: listenerSwitch._peerInfo + }) + + connection.once('private', (conn) => { + expect(conn).to.be.an.instanceof(Connection) + done() + }) + + connection.once('connected', (conn) => { + expect(conn).to.be.an.instanceof(Connection) + connection.protect() + }) + + connection.dial() + }) + + it('should be able to encrypt a protected connection', (done) => { + const connection = new ConnectionFSM({ + _switch: dialerSwitch, + peerInfo: listenerSwitch._peerInfo + }) + + connection.once('connected', (conn) => { + expect(conn).to.be.an.instanceof(Connection) + connection.protect() + }) + connection.once('private', (conn) => { + expect(conn).to.be.an.instanceof(Connection) + connection.encrypt() + }) + connection.once('encrypted', (conn) => { + expect(conn).to.be.an.instanceof(Connection) + done() + }) + + connection.dial() + }) + }) +}) \ No newline at end of file diff --git a/test/node.js b/test/node.js index 9f624d6..127a5b1 100644 --- a/test/node.js +++ b/test/node.js @@ -1,5 +1,6 @@ 'use strict' +require('./connection.node') require('./pnet.node') require('./transports.node') require('./stream-muxers.node') From 5848e6d0d0347292b945318678141e3ef157cdb8 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Tue, 25 Sep 2018 19:41:13 +0200 Subject: [PATCH 05/27] feat: functioning incoming connection fsm fix: better support custom handlers --- Connections.md | 7 + src/connection/base.js | 67 +++ src/connection/handler.js | 161 ++++++ src/{connection.js => connection/index.js} | 59 +-- .../manager.js} | 6 +- src/dial.old.js | 495 ------------------ src/{dial.js => dialer.js} | 17 +- src/index.js | 27 +- src/protocol-muxer.js | 4 +- src/transport.js | 22 +- 10 files changed, 273 insertions(+), 592 deletions(-) create mode 100644 src/connection/base.js create mode 100644 src/connection/handler.js rename src/{connection.js => connection/index.js} (91%) rename src/{connection-manager.js => connection/manager.js} (97%) delete mode 100644 src/dial.old.js rename src/{dial.js => dialer.js} (84%) diff --git a/Connections.md b/Connections.md index a88984f..2062a3f 100644 --- a/Connections.md +++ b/Connections.md @@ -33,3 +33,10 @@ Encrypted Connection -> .upgrade -> upgraded connection *upgraded* or Encrypted Connection -> .upgrade -> Encrypted Connection -> .shake(protocol) -> Connected _cannot reuse_ *connection* Upgraded Connection -> .shake(protocol) -> new stream _upraded conn can be used_ *stream* + + +## Incoming connections +1. Transport.listener gives us a basic connection +2. We privatize the connection, if needed +3. We must handle encyption muxing first +4. We then handle protocol muxing \ No newline at end of file diff --git a/src/connection/base.js b/src/connection/base.js new file mode 100644 index 0000000..640926b --- /dev/null +++ b/src/connection/base.js @@ -0,0 +1,67 @@ +'use strict' + +const EventEmitter = require('events').EventEmitter +const debug = require('debug') + +class BaseConnection extends EventEmitter { + constructor({ _switch, logName }) { + super() + + this.switch = _switch + this.ourPeerInfo = this.switch._peerInfo + this.log = debug(logName) + } + + /** + * Puts the state into encrypting mode + * + * @returns {void} + */ + encrypt () { + this._state('encrypt') + } + + /** + * Puts the state into privatizing mode + * + * @returns {void} + */ + protect () { + this._state('privatize') + } + + /** + * Puts the state into muxing mode + * + * @returns {void} + */ + upgrade () { + this._state('upgrade') + } + + /** + * Wraps this.conn with the Switch.protector for private connections + * + * @private + * @fires ConnectionFSM#error + * @returns {void} + */ + _onPrivatizing () { + if (!this.switch.protector) { + return this._state('done') + } + + this.conn = this.switch.protector.protect(this.conn, (err) => { + if (err) { + this.emit('error', err) + return this._state('disconnect') + } + + this.log(`successfully privatized conn to ${this.theirB58Id}`) + this.conn.setPeerInfo(this.theirPeerInfo) + this._state('done') + }) + } +} + +module.exports = BaseConnection \ No newline at end of file diff --git a/src/connection/handler.js b/src/connection/handler.js new file mode 100644 index 0000000..e99768c --- /dev/null +++ b/src/connection/handler.js @@ -0,0 +1,161 @@ +'use strict' + +const FSM = require('fsm-event') +const debug = require('debug') +const multistream = require('multistream-select') + +const observeConn = require('../observe-connection') +const BaseConnection = require('./base') + +class IncomingConnectionFSM extends BaseConnection { + constructor ({ connection, _switch, transportKey }) { + super({ + _switch, + logName: `libp2p:switch:inc_connection:${_switch._peerInfo.id.toB58String().slice(0, 8)}` + }) + this.conn = connection + this.theirPeerInfo = null + this.ourPeerInfo = this.switch._peerInfo + this.transportKey = transportKey + this.protocolMuxer = this.switch.protocolMuxer(this.transportKey) + + this._state = FSM('DIALED', { + DISCONNECTED: { }, + DIALED: { // Base connection to peer established + privatize: 'PRIVATIZING', + encrypt: 'ENCRYPTING' + }, + PRIVATIZING: { // Protecting the base connection + done: 'PRIVATIZED', + disconnect: 'DISCONNECTING' + }, + PRIVATIZED: { // Base connection is protected + encrypt: 'ENCRYPTING' + }, + ENCRYPTING: { // Encrypting the base connection + done: 'ENCRYPTED', + disconnect: 'DISCONNECTING' + }, + ENCRYPTED: { // Upgrading could not happen, the connection is encrypted and waiting + upgrade: 'UPGRADING', + disconnect: 'DISCONNECTING' + }, + UPGRADING: { // Attempting to upgrade the connection with muxers + done: 'MUXED' + }, + MUXED: { + disconnect: 'DISCONNECTING' + }, + DISCONNECTING: { // Shutting down the connection + done: 'DISCONNECTED' + } + }) + + this._state.on('PRIVATIZING', () => this._onPrivatizing()) + this._state.on('PRIVATIZED', () => this._onPrivatized()) + this._state.on('ENCRYPTING', () => this._onEncrypting()) + this._state.on('ENCRYPTED', () => { + this.log(`successfully encrypted connection to ${this.theirB58Id || 'unknown peer'}`) + this.emit('encrypted', this.conn) + }) + this._state.on('UPGRADING', () => this._onUpgrading()) + this._state.on('MUXED', () => { + this.log(`successfully muxed connection to ${this.theirB58Id || 'unknown peer'}`) + this.emit('muxed', this.conn) + }) + this._state.on('DISCONNECTING', () => { + if (this.theirPeerInfo) { + this.theirPeerInfo.disconnect() + } + }) + } + + /** + * Gets the current state of the connection + * + * @returns {string} The current state of the connection + */ + getState () { + return this._state._state + } + + // TODO: We need to handle N+1 crypto libraries + _onEncrypting () { + // If the connection is for a specific transport, observe it + if (this.transportKey) { + this.conn = observeConn(this.transportKey, null, this.conn, this.switch.observer) + } + + this.log(`encrypting connection via ${this.switch.crypto.tag}`) + + const ms = new multistream.Listener() + + ms.addHandler(this.switch.crypto.tag, (protocol, _conn) => { + const conn = observeConn(null, protocol, _conn, this.switch.observer) + this.conn = this.switch.crypto.encrypt(this.ourPeerInfo.id, conn, undefined, (err) => { + if (err) { + this.emit('error', err) + return this._state('disconnect') + } + this.conn.getPeerInfo((_, peerInfo) => { + this.theirPeerInfo = peerInfo + this._state('done') + }) + }) + }, null) + + ms.handle(this.conn, (err) => { + if (err) { + this.emit('crypto handshaking failed', err) + } + }) + } + + _onPrivatized () { + this.log(`successfully privatized incoming connection`) + this.emit('private', this.conn) + } + + _onUpgrading () { + this.log('adding the protocol muxer to the connection') + this.protocolMuxer(this.conn) + this._state('done') + } +} + +function listener (_switch) { + const log = debug(`libp2p:switch:listener`) + + /** + * Takes a transport key and returns a connection handler function + * + * @param {string} transportKey The key of the transport to handle connections for + * @param {function} handler A custom handler to use + * @returns {function(Connection)} A connection handler function + */ + return (transportKey, handler) => { + /** + * Takes a base connection and manages listening behavior + * + * @param {Connection} connection The connection to manage + * @returns {void} + */ + return (connection) => { + log('received incoming connection') + const connFSM = new IncomingConnectionFSM({ connection, _switch, transportKey }) + + connFSM.once('error', (err) => log(err)) + connFSM.once('private', (conn) => { + if (handler) { + return handler(conn) + } + connFSM.encrypt() + }) + connFSM.once('encrypted', () => connFSM.upgrade()) + + connFSM.protect() + } + } +} + +module.exports = listener \ No newline at end of file diff --git a/src/connection.js b/src/connection/index.js similarity index 91% rename from src/connection.js rename to src/connection/index.js index 4ecd6dd..1471b01 100644 --- a/src/connection.js +++ b/src/connection/index.js @@ -1,14 +1,13 @@ 'use strict' const FSM = require('fsm-event') -const EventEmitter = require('events').EventEmitter const setImmediate = require('async/setImmediate') const Circuit = require('libp2p-circuit') const multistream = require('multistream-select') +const BaseConnection = require('./base') -const observeConnection = require('./observe-connection') -const Errors = require('./errors') -const debug = require('debug') +const observeConnection = require('../observe-connection') +const Errors = require('../errors') /** * @typedef {Object} ConnectionOptions @@ -24,7 +23,7 @@ const debug = require('debug') * state machine also helps to improve the ability to handle dial backoff, * coalescing dials and dial locks. */ -class ConnectionFSM extends EventEmitter { +class ConnectionFSM extends BaseConnection { /** * Determines if the given connection is an instance of ConnectionFSM * @@ -41,13 +40,13 @@ class ConnectionFSM extends EventEmitter { * @constructor */ constructor ({ _switch, peerInfo, muxer }) { - super() + super({ + _switch, + logName: `libp2p:switch:connection:${_switch._peerInfo.id.toB58String().slice(0, 8)}` + }) - this.switch = _switch this.theirPeerInfo = peerInfo this.theirB58Id = this.theirPeerInfo.id.toB58String() - this.ourPeerInfo = this.switch._peerInfo - this.log = debug(`libp2p:switch:connection:${this.ourPeerInfo.id.toB58String().slice(0, 8)}`) this.conn = null // The base connection this.muxer = muxer // The upgraded/muxed connection @@ -113,10 +112,7 @@ class ConnectionFSM extends EventEmitter { this._state.on('DIALING', () => this._onDialing()) this._state.on('DIALED', () => this._onDialed()) this._state.on('PRIVATIZING', () => this._onPrivatizing()) - this._state.on('PRIVATIZED', () => { - this.log(`successfully privatized conn to ${this.theirB58Id}`) - this.emit('private', this.conn) - }) + this._state.on('PRIVATIZED', () => this.emit('private', this.conn)) this._state.on('ENCRYPTING', () => this._onEncrypting()) this._state.on('ENCRYPTED', () => { this.log(`successfully encrypted connection to ${this.theirB58Id}`) @@ -160,24 +156,6 @@ class ConnectionFSM extends EventEmitter { this._state('dial') } - /** - * Puts the state into encrypting mode - * - * @returns {void} - */ - encrypt () { - this._state('encrypt') - } - - /** - * Puts the state into privatizing mode - * - * @returns {void} - */ - protect () { - this._state('privatize') - } - /** * Initiates a handshake for the given protocol * @@ -325,25 +303,6 @@ class ConnectionFSM extends EventEmitter { this._state('done') } - /** - * Wraps this.conn with the Switch.protector for private connections - * - * @private - * @fires ConnectionFSM#error - * @returns {void} - */ - _onPrivatizing () { - this.conn = this.switch.protector.protect(this.conn, (err) => { - if (err) { - this.emit('error', err) - return this._state('disconnect') - } - - this.conn.setPeerInfo(this.theirPeerInfo) - this._state('done') - }) - } - /** * Attempts to encrypt `this.conn` with the Switch's crypto. * diff --git a/src/connection-manager.js b/src/connection/manager.js similarity index 97% rename from src/connection-manager.js rename to src/connection/manager.js index 1773eb8..357c7f6 100644 --- a/src/connection-manager.js +++ b/src/connection/manager.js @@ -10,7 +10,7 @@ const setImmediate = require('async/setImmediate') const Circuit = require('libp2p-circuit') -const plaintext = require('./plaintext') +const plaintext = require('../plaintext') /** * Contains methods for binding handlers to the Switch @@ -104,7 +104,7 @@ class ConnectionManager { } peerInfo = this.switch._peerBook.put(peerInfo) - muxedConn.on('close', () => { + muxedConn.once('close', () => { delete this.switch.muxedConns[b58Str] peerInfo.disconnect() peerInfo = this.switch._peerBook.put(peerInfo) @@ -123,7 +123,7 @@ class ConnectionManager { /** * Adds the `encrypt` handler for the given `tag` and also sets the - * Switch's crypto to past `encrypt` function + * Switch's crypto to passed `encrypt` function * * @param {String} tag * @param {function(PeerID, Connection, PeerId, Callback)} encrypt diff --git a/src/dial.old.js b/src/dial.old.js deleted file mode 100644 index d1fbe81..0000000 --- a/src/dial.old.js +++ /dev/null @@ -1,495 +0,0 @@ -'use strict' - -const multistream = require('multistream-select') -const Connection = require('interface-connection').Connection -const setImmediate = require('async/setImmediate') -const Circuit = require('libp2p-circuit') -const waterfall = require('async/waterfall') - -const debug = require('debug') -const log = debug('libp2p:switch:dial') - -const getPeerInfo = require('./get-peer-info') -const observeConnection = require('./observe-connection') -const UNEXPECTED_END = 'Unexpected end of input from reader.' - -/** - * Uses the given MultistreamDialer to select the protocol matching the given key - * - * A helper method to catch errors from pull streams ending unexpectedly - * Needed until https://github.com/dignifiedquire/pull-length-prefixed/pull/8 is merged. - * - * @param {MultistreamDialer} msDialer a multistream.Dialer - * @param {string} key The key type to select - * @param {function(Error)} callback Used for standard async flow - * @param {function(Error)} abort A callback to be used for ending the connection outright - * @returns {void} - */ -function selectSafe (msDialer, key, callback, abort) { - msDialer.select(key, (err, conn) => { - if (err === true) { - return abort(new Error(UNEXPECTED_END)) - } - - callback(err, conn) - }) -} - -/** - * Uses the given MultistreamDialer to handle the given connection - * - * A helper method to catch errors from pull streams ending unexpectedly - * Needed until https://github.com/dignifiedquire/pull-length-prefixed/pull/8 is merged - * - * @param {MultistreamDialer} msDialer - * @param {Connection} connection The connection to handle - * @param {function(Error)} callback Used for standard async flow - * @param {function(Error)} abort A callback to be used for ending the connection outright - * @returns {void} - */ -function handleSafe (msDialer, connection, callback, abort) { - msDialer.handle(connection, (err) => { - // Repackage errors from pull-streams ending unexpectedly. - // Needed until https://github.com/dignifiedquire/pull-length-prefixed/pull/8 is merged. - if (err === true) { - return abort(new Error(UNEXPECTED_END)) - } - - callback(err) - }) -} - -/** - * Manages dialing to another peer, including muxer upgrades - * and crypto management. The main entry point for dialing is - * Dialer.dial - * - * @param {Switch} _switch - * @param {PeerInfo} peerInfo - * @param {string} protocol - * @param {function(Error, Connection)} callback - */ -class Dialer { - constructor (_switch, peerInfo, ourPeerInfo, protocol, callback) { - this.switch = _switch - this.peerInfo = peerInfo - this.ourPeerInfo = ourPeerInfo - this.protocol = protocol - this.callback = callback - } - - /** - * Initializes a proxy connection and returns it. The connection is also immediately - * dialed. This will include establishing the base connection, crypto, muxing and the - * protocol handshake if all needed components have already been set. - * - * @returns {Connection} - */ - dial () { - const proxyConnection = new Connection() - proxyConnection.setPeerInfo(this.peerInfo) - - waterfall([ - (cb) => { - this._establishConnection(cb) - }, - (connection, cb) => { - if (connection) { - proxyConnection.setPeerInfo(this.peerInfo) - proxyConnection.setInnerConn(connection) - return cb(null, proxyConnection) - } - cb(null) - } - ], (err, connection) => { - if ((err && err.message === UNEXPECTED_END) || err === true) { - log('Connection dropped for %s', this.peerInfo.id.toB58String()) - return this.callback(null, null) - } - - this.callback(err, connection) - }) - - return proxyConnection - } - - /** - * Establishes a base connection and then continues to upgrade that connection - * including: crypto, muxing and the protocol handshake. If any upgrade is not - * yet available, or already exists, the upgrade will continue where it left off. - * - * @private - * @param {function(Error, Connection)} callback - * @returns {void} - */ - _establishConnection (callback) { - const b58Id = this.peerInfo.id.toB58String() - log('dialing %s', b58Id) - if (b58Id === this.ourPeerInfo.id.toB58String()) { - return callback(new Error('A node cannot dial itself')) - } - - waterfall([ - (cb) => { - // Start with a base connection, which includes encryption - this._createBaseConnection(b58Id, cb) - }, - (baseConnection, cb) => { - // Upgrade the connection with a muxer - this._createMuxedConnection(baseConnection, b58Id, cb) - }, - (muxer, cb) => { - // If we have no protocol, dont continue with the handshake - if (!this.protocol) { - return cb() - } - - // If we have a muxer, create a new stream, otherwise it's a standard connection - if (muxer.newStream) { - muxer.newStream((err, conn) => { - if (err) return cb(err) - - this._performProtocolHandshake(conn, cb) - }) - return - } - - this._performProtocolHandshake(muxer, cb) - } - ], (err, connection) => { - callback(err, connection) - }) - } - - /** - * If the base connection already exists to the PeerId key, `b58Id`, - * it will be returned in the callback. If no connection exists, one will - * be attempted via Dialer.attemptDial. - * - * @private - * @param {string} b58Id - * @param {function(Error, Connection)} callback - * @returns {void} - */ - _createBaseConnection (b58Id, callback) { - const baseConnection = this.switch.conns[b58Id] - const muxedConnection = this.switch.muxedConns[b58Id] - - // if the muxed connection exists, dont return a connection, - // _createMuxedConnection will get the connection - if (muxedConnection) { - return callback(null, null) - } - if (baseConnection) { - this.switch.conns[b58Id] = undefined - return callback(null, baseConnection) - } - - waterfall([ - (cb) => { - this._attemptDial(cb) - }, - (baseConnection, cb) => { - // Create a private connection if it's needed - this._createPrivateConnection(baseConnection, cb) - }, - (connection, cb) => { - // Add the Switch's crypt encryption to the connection - this._encryptConnection(connection, cb) - } - ], (err, encryptedConnection) => { - if (err) { - return callback(err) - } - - callback(null, encryptedConnection) - }) - } - - /** - * If the switch has a private network protector, `switch.protector`, its `protect` - * method will be called with the given connection. The resulting, wrapped connection - * will be returned via the callback. - * - * @param {Connection} connection The connection to protect - * @param {function(Error, Connection)} callback - * @returns {void} - */ - _createPrivateConnection (connection, callback) { - if (this.switch.protector === null) { - return callback(null, connection) - } - - // If the switch has a protector, be private - const protectedConnection = this.switch.protector.protect(connection, (err) => { - if (err) { - return callback(err) - } - - protectedConnection.setPeerInfo(this.peerInfo) - callback(null, protectedConnection) - }) - } - - /** - * If the given PeerId key, `b58Id`, has an existing muxed connection - * it will be returned via the callback, otherwise the connection - * upgrade will be initiated via Dialer.attemptMuxerUpgrade. - * - * @private - * @param {Connection} connection - * @param {string} b58Id - * @param {function(Error, Connection)} callback - * @returns {void} - */ - _createMuxedConnection (connection, b58Id, callback) { - const muxedConnection = this.switch.muxedConns[b58Id] - if (muxedConnection) { - return callback(null, muxedConnection.muxer) - } - - connection.setPeerInfo(this.peerInfo) - this._attemptMuxerUpgrade(connection, b58Id, (err, muxer) => { - if (err && !this.protocol) { - this.switch.conns[b58Id] = connection - return callback(null, null) - } - - if (err) { - log('muxer upgrade failed with error', err) - // couldn't upgrade to Muxer, it is ok, use the existing connection - return callback(null, connection) - } - - callback(null, muxer) - }, callback) - } - - /** - * Iterates over each Muxer on the Switch and attempts to upgrade - * the given `connection`. Successful muxed connections will be stored - * on the Switch.muxedConns with `b58Id` as their key for future reference. - * - * @private - * @param {Connection} connection - * @param {string} b58Id - * @param {function(Error, Connection)} callback - * @param {function(Error, Connection)} abort A callback to be used for ending the connection outright - * @returns {void} - */ - _attemptMuxerUpgrade (connection, b58Id, callback, abort) { - const muxers = Object.keys(this.switch.muxers) - - if (muxers.length === 0) { - return callback(new Error('no muxers available')) - } - - const msDialer = new multistream.Dialer() - handleSafe(msDialer, connection, (err) => { - if (err) { - return callback(new Error('multistream not supported')) - } - - // 1. try to handshake in one of the muxers available - // 2. if succeeds - // - add the muxedConn to the list of muxedConns - // - add incomming new streams to connHandler - const nextMuxer = (key) => { - log('selecting %s', key) - selectSafe(msDialer, key, (err, _conn) => { - if (err) { - if (muxers.length === 0) { - return callback(new Error('could not upgrade to stream muxing')) - } - - return nextMuxer(muxers.shift()) - } - - // observe muxed connections - const conn = observeConnection(null, key, _conn, this.switch.observer) - - const muxedConn = this.switch.muxers[key].dialer(conn) - this.switch.muxedConns[b58Id] = { - muxer: muxedConn - } - - muxedConn.once('close', () => { - delete this.switch.muxedConns[b58Id] - this.peerInfo.disconnect() - this.switch._peerInfo.disconnect() - log(`closed connection to ${b58Id}`) - setImmediate(() => this.switch.emit('peer-mux-closed', this.peerInfo)) - }) - - // For incoming streams, in case identify is on - muxedConn.on('stream', (conn) => { - conn.setPeerInfo(this.peerInfo) - this.switch.protocolMuxer(null)(conn) - }) - - setImmediate(() => this.switch.emit('peer-mux-established', this.peerInfo)) - - callback(null, muxedConn) - }, abort) - } - - nextMuxer(muxers.shift()) - }, abort) - } - - /** - * Iterates over each Transport on the Switch and attempts to connect - * to the peer. Once a Transport succeeds, no additional Transports will - * be dialed. - * - * @private - * @param {function(Error, Connection)} callback - * @returns {void} - */ - _attemptDial (callback) { - if (!this.switch.hasTransports()) { - return callback(new Error('No transports registered, dial not possible')) - } - - const tKeys = this.switch.availableTransports(this.peerInfo) - - const circuitEnabled = Boolean(this.switch.transports[Circuit.tag]) - let circuitTried = false - - const nextTransport = (key) => { - let transport = key - const b58Id = this.peerInfo.id.toB58String() - if (!transport) { - if (!circuitEnabled) { - const msg = `Circuit not enabled and all transports failed to dial peer ${b58Id}!` - return callback(new Error(msg)) - } - - if (circuitTried) { - return callback(new Error(`No available transports to dial peer ${b58Id}!`)) - } - - log(`Falling back to dialing over circuit`) - this.peerInfo.multiaddrs.add(`/p2p-circuit/ipfs/${b58Id}`) - circuitTried = true - transport = Circuit.tag - } - - log(`dialing transport ${transport}`) - this.switch.transport.dial(transport, this.peerInfo, (err, _conn) => { - if (err) { - log(err) - return nextTransport(tKeys.shift()) - } - - const conn = observeConnection(transport, null, _conn, this.switch.observer) - callback(null, conn) - }) - } - - nextTransport(tKeys.shift()) - } - - /** - * Attempts to encrypt the given `connection` with the Switch's crypto. - * - * @private - * @param {Connection} connection - * @param {function(Error, Connection)} callback - * @returns {void} - */ - _encryptConnection (connection, callback) { - const msDialer = new multistream.Dialer() - handleSafe(msDialer, connection, (err) => { - if (err) { - return callback(err) - } - - const myId = this.switch._peerInfo.id - log('selecting crypto: %s', this.switch.crypto.tag) - - selectSafe(msDialer, this.switch.crypto.tag, (err, _conn) => { - if (err) { - return callback(err) - } - - const conn = observeConnection(null, this.switch.crypto.tag, _conn, this.switch.observer) - - const encryptedConnection = this.switch.crypto.encrypt(myId, conn, this.peerInfo.id, (err) => { - if (err) { - return callback(err) - } - - encryptedConnection.setPeerInfo(this.peerInfo) - callback(null, encryptedConnection) - }) - }, callback) - }, callback) - } - - /** - * Initiates a handshake for the Dialer's set protocol - * - * @private - * @param {Connection} connection - * @param {function(Error, Connection)} callback - * @returns {void} - */ - _performProtocolHandshake (connection, callback) { - // If there is no protocol set yet, don't perform the handshake - if (!this.protocol) { - callback() - } - - console.log(connection.peerInfo, connection.info.peerInfo) - - const msDialer = new multistream.Dialer() - handleSafe(msDialer, connection, (err) => { - if (err) { - return callback(err) - } - - selectSafe(msDialer, this.protocol, (err, _conn) => { - if (err) { - log(`could not perform protocol handshake: `, err) - return callback(err) - } - const conn = observeConnection(null, this.protocol, _conn, this.switch.observer) - callback(null, conn) - }, callback) - }, callback) - } -} - -/** - * Returns a Dialer generator that when called, will immediately begin dialing - * to the given `peer`. - * - * @param {Switch} _switch - * @returns {function(PeerInfo, string, function(Error, Connection))} - */ -function dial (_switch) { - /** - * Creates a new dialer and immediately begins dialing to the given `peer` - * - * @param {PeerInfo} peer - * @param {string} protocol - * @param {function(Error, Connection)} callback - * @returns {Connection} - */ - return (peer, protocol, callback) => { - if (typeof protocol === 'function') { - callback = protocol - protocol = null - } - - callback = callback || function noop () {} - - const peerInfo = getPeerInfo(peer, _switch._peerBook) - const dialer = new Dialer(_switch, peerInfo, _switch._peerInfo, protocol, callback) - - return dialer.dial() - } -} - -module.exports = dial diff --git a/src/dial.js b/src/dialer.js similarity index 84% rename from src/dial.js rename to src/dialer.js index acefea7..2ea8c46 100644 --- a/src/dial.js +++ b/src/dialer.js @@ -4,7 +4,6 @@ const Connection = require('interface-connection').Connection const ConnectionFSM = require('./connection') const getPeerInfo = require('./get-peer-info') const once = require('once') -const setImmediate = require('async/setImmediate') const debug = require('debug') const log = debug('libp2p:switch:dial') @@ -56,7 +55,6 @@ function dial (_switch) { let connection = _switch.muxedConns[b58Id] || _switch.conns[b58Id] - // TODO: make the listen logic of switch create an fsm if (!ConnectionFSM.isConnection(connection)) { connection = new ConnectionFSM({ _switch, @@ -65,22 +63,11 @@ function dial (_switch) { }) } - // TODO: Add listeners to the connection to control its state - - // TODO: Add logic here for handling previous connections const proxyConnection = new Connection() proxyConnection.setPeerInfo(peerInfo) - connection.once('error', (err) => { - // console.log('got an err', err) - callback(err) - }) - connection.once('connected', () => { - if (_switch.protector) { - return connection.protect() - } - connection.encrypt() - }) + connection.once('error', (err) => callback(err)) + connection.once('connected', () => connection.protect()) connection.once('private', () => connection.encrypt()) connection.once('encrypted', () => connection.upgrade()) connection.once('muxed', () => { diff --git a/src/index.js b/src/index.js index c3b0aaf..eedb048 100644 --- a/src/index.js +++ b/src/index.js @@ -5,9 +5,10 @@ const EventEmitter = require('events').EventEmitter const each = require('async/each') const series = require('async/series') const TransportManager = require('./transport') -const ConnectionManager = require('./connection-manager') +const ConnectionManager = require('./connection/manager') const getPeerInfo = require('./get-peer-info') -const dial = require('./dial') +const dial = require('./dialer') +const connectionHandler = require('./connection/handler') const ProtocolMuxer = require('./protocol-muxer') const plaintext = require('./plaintext') const Observer = require('./observer') @@ -79,6 +80,9 @@ class Switch extends EventEmitter { // higher level (public) API this.dial = dial(this) + // All purpose connection handler for managing incoming connections + this._connectionHandler = connectionHandler(this) + // Setup the internal state this.state = new FSM('STOPPED', { STOPPED: { @@ -257,13 +261,18 @@ class Switch extends EventEmitter { return cb() } - conn.muxer.end((err) => { - // If OK things are fine, and someone just shut down - if (err && err.message !== 'Fatal error: OK') { - return cb(err) - } - cb() - }) + try { + conn.muxer.end((err) => { + // If OK things are fine, and someone just shut down + if (err && err.message !== 'Fatal error: OK') { + return cb(err) + } + cb() + }) + } catch (err) { + console.log('Ka-blewy') + cb(err) + } }, cb), (cb) => { each(this.transports, (transport, cb) => { diff --git a/src/protocol-muxer.js b/src/protocol-muxer.js index dce7f9f..0344c7e 100644 --- a/src/protocol-muxer.js +++ b/src/protocol-muxer.js @@ -5,6 +5,7 @@ const observeConn = require('./observe-connection') const debug = require('debug') const log = debug('libp2p:switch:protocol-muxer') +log.error = debug('libp2p:switch:protocol-muxer:error') module.exports = function protocolMuxer (protocols, observer) { return (transport) => (_parentConn) => { @@ -35,8 +36,9 @@ module.exports = function protocolMuxer (protocols, observer) { }) ms.handle(parentConn, (err) => { + // TODO: handle successful and failed connections for the FSM if (err) { - // the multistream handshake failed + log.error(`multistream handshake failed`, err) } }) } diff --git a/src/transport.js b/src/transport.js index 9a9c211..5dbc3dd 100644 --- a/src/transport.js +++ b/src/transport.js @@ -101,29 +101,13 @@ class TransportManager { * If a `handler` is not provided, the Switch's `protocolMuxer` will be used. * * @param {String} key - * @param {*} options + * @param {*} _options Currently ignored * @param {function(Connection)} handler * @param {function(Error)} callback * @returns {void} */ - listen (key, options, handler, callback) { - let muxerHandler - - // if no handler is passed, we pass conns to protocolMuxer - if (!handler) { - handler = this.switch.protocolMuxer(key) - } - - // If we have a protector make the connection private - if (this.switch.protector) { - muxerHandler = handler - handler = (parentConnection) => { - const connection = this.switch.protector.protect(parentConnection, () => { - // If we get an error here, we should stop talking to this peer - muxerHandler(connection) - }) - } - } + listen (key, _options, handler, callback) { + handler = this.switch._connectionHandler(key, handler) const transport = this.switch.transports[key] const multiaddrs = TransportManager.dialables( From 45ae65b339e7d7ae31ce0182feaca4ce2fdac540 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Tue, 25 Sep 2018 23:31:35 +0200 Subject: [PATCH 06/27] fix: linting --- src/connection/base.js | 6 +++--- src/connection/handler.js | 2 +- src/connection/index.js | 9 +++++---- src/dialer.js | 2 +- src/transport.js | 2 +- test/circuit-relay.node.js | 1 - test/connection.node.js | 5 +---- 7 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/connection/base.js b/src/connection/base.js index 640926b..5cea521 100644 --- a/src/connection/base.js +++ b/src/connection/base.js @@ -4,7 +4,7 @@ const EventEmitter = require('events').EventEmitter const debug = require('debug') class BaseConnection extends EventEmitter { - constructor({ _switch, logName }) { + constructor ({ _switch, logName }) { super() this.switch = _switch @@ -39,7 +39,7 @@ class BaseConnection extends EventEmitter { this._state('upgrade') } - /** + /** * Wraps this.conn with the Switch.protector for private connections * * @private @@ -64,4 +64,4 @@ class BaseConnection extends EventEmitter { } } -module.exports = BaseConnection \ No newline at end of file +module.exports = BaseConnection diff --git a/src/connection/handler.js b/src/connection/handler.js index e99768c..907f046 100644 --- a/src/connection/handler.js +++ b/src/connection/handler.js @@ -158,4 +158,4 @@ function listener (_switch) { } } -module.exports = listener \ No newline at end of file +module.exports = listener diff --git a/src/connection/index.js b/src/connection/index.js index 1471b01..4431c8c 100644 --- a/src/connection/index.js +++ b/src/connection/index.js @@ -31,7 +31,7 @@ class ConnectionFSM extends BaseConnection { * @param {*} connection * @returns {boolean} */ - static isConnection(connection) { + static isConnection (connection) { return connection instanceof ConnectionFSM } @@ -48,7 +48,7 @@ class ConnectionFSM extends BaseConnection { this.theirPeerInfo = peerInfo this.theirB58Id = this.theirPeerInfo.id.toB58String() - this.conn = null // The base connection + this.conn = null // The base connection this.muxer = muxer // The upgraded/muxed connection // TODO: If given a muxer, we need to set the state @@ -142,7 +142,7 @@ class ConnectionFSM extends BaseConnection { return this._state._state } - /** + /** * Puts the state into dialing mode * * @fires ConnectionFSM#Error May emit a DIAL_SELF error @@ -251,7 +251,7 @@ class ConnectionFSM extends BaseConnection { nextTransport(tKeys.shift()) } - /** + /** * Once a connection has been successfully dialed, the connection * will be privatized or encrypted depending on the presence of the * Switch.protector. @@ -410,6 +410,7 @@ class ConnectionFSM extends BaseConnection { * needs to go. * * @param {Error} err + * @returns {void} */ _didUpgrade (err) { if (err) { diff --git a/src/dialer.js b/src/dialer.js index 2ea8c46..aa52252 100644 --- a/src/dialer.js +++ b/src/dialer.js @@ -51,7 +51,7 @@ function dial (_switch) { const peerInfo = getPeerInfo(peer, _switch._peerBook) const b58Id = peerInfo.id.toB58String() - log(`${_switch._peerInfo.id.toB58String().slice(0,8)} dial request to ${b58Id.slice(0,8)} with protocol ${protocol}`) + log(`${_switch._peerInfo.id.toB58String().slice(0, 8)} dial request to ${b58Id.slice(0, 8)} with protocol ${protocol}`) let connection = _switch.muxedConns[b58Id] || _switch.conns[b58Id] diff --git a/src/transport.js b/src/transport.js index 5dbc3dd..4f4bc7b 100644 --- a/src/transport.js +++ b/src/transport.js @@ -52,7 +52,7 @@ class TransportManager { * @returns {void} */ remove (key, callback) { - callback = callback || function() {} + callback = callback || function () {} if (!this.switch.transports[key]) { return callback() diff --git a/test/circuit-relay.node.js b/test/circuit-relay.node.js index fddc3e5..d263997 100644 --- a/test/circuit-relay.node.js +++ b/test/circuit-relay.node.js @@ -19,7 +19,6 @@ const getPorts = require('portfinder').getPorts const utils = require('./utils') const createInfos = utils.createInfos const Swarm = require('../src') -const debug = require('debug') describe(`circuit`, function () { let swarmA // TCP and WS diff --git a/test/connection.node.js b/test/connection.node.js index 425cf9b..8902e3f 100644 --- a/test/connection.node.js +++ b/test/connection.node.js @@ -5,7 +5,6 @@ const chai = require('chai') const dirtyChai = require('dirty-chai') const expect = chai.expect chai.use(dirtyChai) -const sinon = require('sinon') const PeerBook = require('peer-book') const WS = require('libp2p-websockets') const parallel = require('async/parallel') @@ -21,9 +20,7 @@ generatePSK(psk) const ConnectionFSM = require('../src/connection') const Switch = require('../src') -const Errors = require('../src/errors') const createInfos = require('./utils').createInfos -const tryEcho = require('./utils').tryEcho describe('ConnectionFSM', () => { let listenerSwitch @@ -263,4 +260,4 @@ describe('ConnectionFSM', () => { connection.dial() }) }) -}) \ No newline at end of file +}) From 78203fdc5da43ca73895912ca244f3ff2936a489 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 26 Sep 2018 14:45:29 +0200 Subject: [PATCH 07/27] fix: stats --- src/connection/base.js | 4 ++-- src/connection/handler.js | 32 +++++++++++++++----------------- src/connection/index.js | 2 +- src/protocol-muxer.js | 16 ++++++++++------ 4 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/connection/base.js b/src/connection/base.js index 5cea521..b33e20a 100644 --- a/src/connection/base.js +++ b/src/connection/base.js @@ -4,12 +4,12 @@ const EventEmitter = require('events').EventEmitter const debug = require('debug') class BaseConnection extends EventEmitter { - constructor ({ _switch, logName }) { + constructor ({ _switch, name }) { super() this.switch = _switch this.ourPeerInfo = this.switch._peerInfo - this.log = debug(logName) + this.log = debug(`libp2p:conn:${name}`) } /** diff --git a/src/connection/handler.js b/src/connection/handler.js index 907f046..648bf25 100644 --- a/src/connection/handler.js +++ b/src/connection/handler.js @@ -11,13 +11,14 @@ class IncomingConnectionFSM extends BaseConnection { constructor ({ connection, _switch, transportKey }) { super({ _switch, - logName: `libp2p:switch:inc_connection:${_switch._peerInfo.id.toB58String().slice(0, 8)}` + name: `inc:${_switch._peerInfo.id.toB58String().slice(0, 8)}` }) this.conn = connection this.theirPeerInfo = null this.ourPeerInfo = this.switch._peerInfo this.transportKey = transportKey this.protocolMuxer = this.switch.protocolMuxer(this.transportKey) + this.msListener = new multistream.Listener() this._state = FSM('DIALED', { DISCONNECTED: { }, @@ -81,18 +82,10 @@ class IncomingConnectionFSM extends BaseConnection { // TODO: We need to handle N+1 crypto libraries _onEncrypting () { - // If the connection is for a specific transport, observe it - if (this.transportKey) { - this.conn = observeConn(this.transportKey, null, this.conn, this.switch.observer) - } - this.log(`encrypting connection via ${this.switch.crypto.tag}`) - const ms = new multistream.Listener() - - ms.addHandler(this.switch.crypto.tag, (protocol, _conn) => { - const conn = observeConn(null, protocol, _conn, this.switch.observer) - this.conn = this.switch.crypto.encrypt(this.ourPeerInfo.id, conn, undefined, (err) => { + this.msListener.addHandler(this.switch.crypto.tag, (protocol, _conn) => { + this.conn = this.switch.crypto.encrypt(this.ourPeerInfo.id, _conn, undefined, (err) => { if (err) { this.emit('error', err) return this._state('disconnect') @@ -104,7 +97,8 @@ class IncomingConnectionFSM extends BaseConnection { }) }, null) - ms.handle(this.conn, (err) => { + // Start handling the connection, this is only needed once + this.msListener.handle(this.conn, (err) => { if (err) { this.emit('crypto handshaking failed', err) } @@ -118,7 +112,7 @@ class IncomingConnectionFSM extends BaseConnection { _onUpgrading () { this.log('adding the protocol muxer to the connection') - this.protocolMuxer(this.conn) + this.protocolMuxer(this.conn, this.msListener) this._state('done') } } @@ -137,17 +131,21 @@ function listener (_switch) { /** * Takes a base connection and manages listening behavior * - * @param {Connection} connection The connection to manage + * @param {Connection} conn The connection to manage * @returns {void} */ - return (connection) => { + return (conn) => { + // Add a transport level observer, if needed + const connection = transportKey ? observeConn(transportKey, null, conn, _switch.observer) : conn + log('received incoming connection') const connFSM = new IncomingConnectionFSM({ connection, _switch, transportKey }) connFSM.once('error', (err) => log(err)) - connFSM.once('private', (conn) => { + connFSM.once('private', (_conn) => { + // Use the custom handler, if it was provided if (handler) { - return handler(conn) + return handler(_conn) } connFSM.encrypt() }) diff --git a/src/connection/index.js b/src/connection/index.js index 4431c8c..b0a8492 100644 --- a/src/connection/index.js +++ b/src/connection/index.js @@ -42,7 +42,7 @@ class ConnectionFSM extends BaseConnection { constructor ({ _switch, peerInfo, muxer }) { super({ _switch, - logName: `libp2p:switch:connection:${_switch._peerInfo.id.toB58String().slice(0, 8)}` + name: `out:${_switch._peerInfo.id.toB58String().slice(0, 8)}` }) this.theirPeerInfo = peerInfo diff --git a/src/protocol-muxer.js b/src/protocol-muxer.js index 0344c7e..aed9fda 100644 --- a/src/protocol-muxer.js +++ b/src/protocol-muxer.js @@ -8,12 +8,16 @@ const log = debug('libp2p:switch:protocol-muxer') log.error = debug('libp2p:switch:protocol-muxer:error') module.exports = function protocolMuxer (protocols, observer) { - return (transport) => (_parentConn) => { - const parentConn = transport - ? observeConn(transport, null, _parentConn, observer) - : _parentConn - - const ms = new multistream.Listener() + return (transport) => (_parentConn, msListener) => { + const ms = msListener || new multistream.Listener() + let parentConn + + // Only observe the transport if we have one, and there is not already a listener + if (transport && !msListener) { + parentConn = observeConn(transport, null, _parentConn, observer) + } else { + parentConn = _parentConn + } Object.keys(protocols).forEach((protocol) => { if (!protocol) { From 0b9f5f13cc445b1286e06f54c5b4057f15e46fe8 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 26 Sep 2018 14:46:36 +0200 Subject: [PATCH 08/27] docs: remove notes --- Connections.md | 42 ------------------------------------------ 1 file changed, 42 deletions(-) delete mode 100644 Connections.md diff --git a/Connections.md b/Connections.md deleted file mode 100644 index 2062a3f..0000000 --- a/Connections.md +++ /dev/null @@ -1,42 +0,0 @@ -# Connections - -Libp2p Switch creates stateful connections between peers. This enables connections to be more easily reused and upgraded. - -## Lifecycle - -### Base Connection -* When no connection exists between peers, a new base connection is created - * Once established the base connection will be privatized, if needed - * Once privatized the connection will be encrypted - -### Muxed Connection -* If a muxer exists on the switch, the base connection will attempt to upgrade - * If the upgrade fails and their is no protocol, upgrading stops and the base connection is saved but not yet used - * If the upgrade fails and their is a protocol, upgrading stops and the base connection is used - * If the upgrade works, the upgraded connnection is used - * Future dial requests will use this connection. - * Future protocol negotiation will use spawned streams from this connection. - -### Protocol Handshaking -* If a protocol was provided on the dial request, handshaking will occur - * If the connection was upgraded (muxed), a new stream is created for the handshake - * If the connection was not upgraded, the current connection is used for the handshake - * If the handshake is successful, the resulting connection will be passed back via the dial calls callback. - - - -No Connection -> .dial -> basic connection *base_connection* -Basic connection -> .protect -> private connection *private* - or Basic Connection -> .encrypt -> encrypted connection *encrypted* -Private Connection -> .encrypt -> encrypted connection *encrypted* -Encrypted Connection -> .upgrade -> upgraded connection *upgraded* - or Encrypted Connection -> .upgrade -> -Encrypted Connection -> .shake(protocol) -> Connected _cannot reuse_ *connection* -Upgraded Connection -> .shake(protocol) -> new stream _upraded conn can be used_ *stream* - - -## Incoming connections -1. Transport.listener gives us a basic connection -2. We privatize the connection, if needed -3. We must handle encyption muxing first -4. We then handle protocol muxing \ No newline at end of file From ef92ee2bfafc8f4445d234dfb4cc41ac0e87c8d9 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 26 Sep 2018 15:14:17 +0200 Subject: [PATCH 09/27] test: bump circuit shutdown timeout --- test/circuit-relay.node.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/circuit-relay.node.js b/test/circuit-relay.node.js index d263997..20b2b93 100644 --- a/test/circuit-relay.node.js +++ b/test/circuit-relay.node.js @@ -260,7 +260,8 @@ describe(`circuit`, function () { }) })) - after((done) => { + after(function (done) { + this.timeout(10000) parallel([ (cb) => bootstrapSwitch.stop(cb), (cb) => tcpSwitch1.stop(cb), From 4b62917de41f6b82b8b1667e3ea7608afff04fa8 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 26 Sep 2018 15:35:30 +0200 Subject: [PATCH 10/27] fix: node 8 support --- src/index.js | 19 +++++++------------ test/circuit-relay.node.js | 17 ++++++++--------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/index.js b/src/index.js index eedb048..4ea63c4 100644 --- a/src/index.js +++ b/src/index.js @@ -261,18 +261,13 @@ class Switch extends EventEmitter { return cb() } - try { - conn.muxer.end((err) => { - // If OK things are fine, and someone just shut down - if (err && err.message !== 'Fatal error: OK') { - return cb(err) - } - cb() - }) - } catch (err) { - console.log('Ka-blewy') - cb(err) - } + conn.muxer.end((err) => { + // If OK things are fine, and someone just shut down + if (err && err.message !== 'Fatal error: OK') { + return cb(err) + } + cb() + }) }, cb), (cb) => { each(this.transports, (transport, cb) => { diff --git a/test/circuit-relay.node.js b/test/circuit-relay.node.js index 20b2b93..3a65c23 100644 --- a/test/circuit-relay.node.js +++ b/test/circuit-relay.node.js @@ -261,7 +261,6 @@ describe(`circuit`, function () { })) after(function (done) { - this.timeout(10000) parallel([ (cb) => bootstrapSwitch.stop(cb), (cb) => tcpSwitch1.stop(cb), @@ -272,9 +271,9 @@ describe(`circuit`, function () { }) it('should be able to dial tcp -> tcp', (done) => { - tcpSwitch2.on('peer-mux-established', function handle (peerInfo) { + tcpSwitch2.on('peer-mux-established', (peerInfo) => { if (peerInfo.id.toB58String() === tcpPeer1.id.toB58String()) { - tcpSwitch2.off('peer-mux-established', handle) + tcpSwitch2.removeAllListeners('peer-mux-established') done() } }) @@ -286,9 +285,9 @@ describe(`circuit`, function () { }) it('should be able to dial tcp -> ws over relay', (done) => { - wsSwitch1.on('peer-mux-established', function handle (peerInfo) { + wsSwitch1.on('peer-mux-established', (peerInfo) => { if (peerInfo.id.toB58String() === tcpPeer1.id.toB58String()) { - wsSwitch1.off('peer-mux-established', handle) + wsSwitch1.removeAllListeners('peer-mux-established') done() } }) @@ -300,9 +299,9 @@ describe(`circuit`, function () { }) it('should be able to dial ws -> ws', (done) => { - wsSwitch2.on('peer-mux-established', function handle (peerInfo) { + wsSwitch2.on('peer-mux-established', (peerInfo) => { if (peerInfo.id.toB58String() === wsPeer1.id.toB58String()) { - wsSwitch2.off('peer-mux-established', handle) + wsSwitch2.removeAllListeners('peer-mux-established') done() } }) @@ -314,9 +313,9 @@ describe(`circuit`, function () { }) it('should be able to dial ws -> tcp over relay', (done) => { - tcpSwitch1.on('peer-mux-established', function handle (peerInfo) { + tcpSwitch1.on('peer-mux-established', (peerInfo) => { if (peerInfo.id.toB58String() === wsPeer2.id.toB58String()) { - tcpSwitch1.off('peer-mux-established', handle) + tcpSwitch1.removeAllListeners('peer-mux-established') expect(Object.keys(tcpSwitch1._peerBook.getAll())).to.include(wsPeer2.id.toB58String()) done() } From 7ab21511f1127e77fafa21482f8925720d273ef2 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 26 Sep 2018 16:21:33 +0200 Subject: [PATCH 11/27] feat: add class-is support for connections --- package.json | 1 + src/connection/base.js | 6 +- src/connection/handler.js | 115 +--------------------------------- src/connection/incoming.js | 124 +++++++++++++++++++++++++++++++++++++ src/connection/index.js | 17 ++--- src/dialer.js | 2 +- 6 files changed, 139 insertions(+), 126 deletions(-) create mode 100644 src/connection/incoming.js diff --git a/package.json b/package.json index 52cd420..5e8a6c3 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "dependencies": { "async": "^2.6.1", "big.js": "^5.1.2", + "class-is": "^1.1.0", "debug": "^3.1.0", "err-code": "^1.1.2", "fsm-event": "^2.1.0", diff --git a/src/connection/base.js b/src/connection/base.js index b33e20a..fb224fa 100644 --- a/src/connection/base.js +++ b/src/connection/base.js @@ -2,6 +2,7 @@ const EventEmitter = require('events').EventEmitter const debug = require('debug') +const withIs = require('class-is') class BaseConnection extends EventEmitter { constructor ({ _switch, name }) { @@ -64,4 +65,7 @@ class BaseConnection extends EventEmitter { } } -module.exports = BaseConnection +module.exports = withIs(BaseConnection, { + className: 'BaseConnection', + symbolName: 'libp2p-switch/BaseConnection', +}) diff --git a/src/connection/handler.js b/src/connection/handler.js index 648bf25..c4c6fd8 100644 --- a/src/connection/handler.js +++ b/src/connection/handler.js @@ -3,119 +3,10 @@ const FSM = require('fsm-event') const debug = require('debug') const multistream = require('multistream-select') +const withIs = require('class-is') +const IncomingConnection = require('./incoming') const observeConn = require('../observe-connection') -const BaseConnection = require('./base') - -class IncomingConnectionFSM extends BaseConnection { - constructor ({ connection, _switch, transportKey }) { - super({ - _switch, - name: `inc:${_switch._peerInfo.id.toB58String().slice(0, 8)}` - }) - this.conn = connection - this.theirPeerInfo = null - this.ourPeerInfo = this.switch._peerInfo - this.transportKey = transportKey - this.protocolMuxer = this.switch.protocolMuxer(this.transportKey) - this.msListener = new multistream.Listener() - - this._state = FSM('DIALED', { - DISCONNECTED: { }, - DIALED: { // Base connection to peer established - privatize: 'PRIVATIZING', - encrypt: 'ENCRYPTING' - }, - PRIVATIZING: { // Protecting the base connection - done: 'PRIVATIZED', - disconnect: 'DISCONNECTING' - }, - PRIVATIZED: { // Base connection is protected - encrypt: 'ENCRYPTING' - }, - ENCRYPTING: { // Encrypting the base connection - done: 'ENCRYPTED', - disconnect: 'DISCONNECTING' - }, - ENCRYPTED: { // Upgrading could not happen, the connection is encrypted and waiting - upgrade: 'UPGRADING', - disconnect: 'DISCONNECTING' - }, - UPGRADING: { // Attempting to upgrade the connection with muxers - done: 'MUXED' - }, - MUXED: { - disconnect: 'DISCONNECTING' - }, - DISCONNECTING: { // Shutting down the connection - done: 'DISCONNECTED' - } - }) - - this._state.on('PRIVATIZING', () => this._onPrivatizing()) - this._state.on('PRIVATIZED', () => this._onPrivatized()) - this._state.on('ENCRYPTING', () => this._onEncrypting()) - this._state.on('ENCRYPTED', () => { - this.log(`successfully encrypted connection to ${this.theirB58Id || 'unknown peer'}`) - this.emit('encrypted', this.conn) - }) - this._state.on('UPGRADING', () => this._onUpgrading()) - this._state.on('MUXED', () => { - this.log(`successfully muxed connection to ${this.theirB58Id || 'unknown peer'}`) - this.emit('muxed', this.conn) - }) - this._state.on('DISCONNECTING', () => { - if (this.theirPeerInfo) { - this.theirPeerInfo.disconnect() - } - }) - } - - /** - * Gets the current state of the connection - * - * @returns {string} The current state of the connection - */ - getState () { - return this._state._state - } - - // TODO: We need to handle N+1 crypto libraries - _onEncrypting () { - this.log(`encrypting connection via ${this.switch.crypto.tag}`) - - this.msListener.addHandler(this.switch.crypto.tag, (protocol, _conn) => { - this.conn = this.switch.crypto.encrypt(this.ourPeerInfo.id, _conn, undefined, (err) => { - if (err) { - this.emit('error', err) - return this._state('disconnect') - } - this.conn.getPeerInfo((_, peerInfo) => { - this.theirPeerInfo = peerInfo - this._state('done') - }) - }) - }, null) - - // Start handling the connection, this is only needed once - this.msListener.handle(this.conn, (err) => { - if (err) { - this.emit('crypto handshaking failed', err) - } - }) - } - - _onPrivatized () { - this.log(`successfully privatized incoming connection`) - this.emit('private', this.conn) - } - - _onUpgrading () { - this.log('adding the protocol muxer to the connection') - this.protocolMuxer(this.conn, this.msListener) - this._state('done') - } -} function listener (_switch) { const log = debug(`libp2p:switch:listener`) @@ -139,7 +30,7 @@ function listener (_switch) { const connection = transportKey ? observeConn(transportKey, null, conn, _switch.observer) : conn log('received incoming connection') - const connFSM = new IncomingConnectionFSM({ connection, _switch, transportKey }) + const connFSM = new IncomingConnection({ connection, _switch, transportKey }) connFSM.once('error', (err) => log(err)) connFSM.once('private', (_conn) => { diff --git a/src/connection/incoming.js b/src/connection/incoming.js new file mode 100644 index 0000000..a1c15b4 --- /dev/null +++ b/src/connection/incoming.js @@ -0,0 +1,124 @@ +'use strict' + +const FSM = require('fsm-event') +const debug = require('debug') +const multistream = require('multistream-select') +const withIs = require('class-is') + +const observeConn = require('../observe-connection') +const BaseConnection = require('./base') + +class IncomingConnectionFSM extends BaseConnection { + constructor ({ connection, _switch, transportKey }) { + super({ + _switch, + name: `inc:${_switch._peerInfo.id.toB58String().slice(0, 8)}` + }) + this.conn = connection + this.theirPeerInfo = null + this.ourPeerInfo = this.switch._peerInfo + this.transportKey = transportKey + this.protocolMuxer = this.switch.protocolMuxer(this.transportKey) + this.msListener = new multistream.Listener() + + this._state = FSM('DIALED', { + DISCONNECTED: { }, + DIALED: { // Base connection to peer established + privatize: 'PRIVATIZING', + encrypt: 'ENCRYPTING' + }, + PRIVATIZING: { // Protecting the base connection + done: 'PRIVATIZED', + disconnect: 'DISCONNECTING' + }, + PRIVATIZED: { // Base connection is protected + encrypt: 'ENCRYPTING' + }, + ENCRYPTING: { // Encrypting the base connection + done: 'ENCRYPTED', + disconnect: 'DISCONNECTING' + }, + ENCRYPTED: { // Upgrading could not happen, the connection is encrypted and waiting + upgrade: 'UPGRADING', + disconnect: 'DISCONNECTING' + }, + UPGRADING: { // Attempting to upgrade the connection with muxers + done: 'MUXED' + }, + MUXED: { + disconnect: 'DISCONNECTING' + }, + DISCONNECTING: { // Shutting down the connection + done: 'DISCONNECTED' + } + }) + + this._state.on('PRIVATIZING', () => this._onPrivatizing()) + this._state.on('PRIVATIZED', () => this._onPrivatized()) + this._state.on('ENCRYPTING', () => this._onEncrypting()) + this._state.on('ENCRYPTED', () => { + this.log(`successfully encrypted connection to ${this.theirB58Id || 'unknown peer'}`) + this.emit('encrypted', this.conn) + }) + this._state.on('UPGRADING', () => this._onUpgrading()) + this._state.on('MUXED', () => { + this.log(`successfully muxed connection to ${this.theirB58Id || 'unknown peer'}`) + this.emit('muxed', this.conn) + }) + this._state.on('DISCONNECTING', () => { + if (this.theirPeerInfo) { + this.theirPeerInfo.disconnect() + } + }) + } + + /** + * Gets the current state of the connection + * + * @returns {string} The current state of the connection + */ + getState () { + return this._state._state + } + + // TODO: We need to handle N+1 crypto libraries + _onEncrypting () { + this.log(`encrypting connection via ${this.switch.crypto.tag}`) + + this.msListener.addHandler(this.switch.crypto.tag, (protocol, _conn) => { + this.conn = this.switch.crypto.encrypt(this.ourPeerInfo.id, _conn, undefined, (err) => { + if (err) { + this.emit('error', err) + return this._state('disconnect') + } + this.conn.getPeerInfo((_, peerInfo) => { + this.theirPeerInfo = peerInfo + this._state('done') + }) + }) + }, null) + + // Start handling the connection, this is only needed once + this.msListener.handle(this.conn, (err) => { + if (err) { + this.emit('crypto handshaking failed', err) + } + }) + } + + _onPrivatized () { + this.log(`successfully privatized incoming connection`) + this.emit('private', this.conn) + } + + _onUpgrading () { + this.log('adding the protocol muxer to the connection') + this.protocolMuxer(this.conn, this.msListener) + this._state('done') + } +} + +module.exports = withIs(IncomingConnectionFSM, { + className: 'IncomingConnectionFSM', + symbolName: 'libp2p-switch/IncomingConnectionFSM', +}) \ No newline at end of file diff --git a/src/connection/index.js b/src/connection/index.js index b0a8492..87cf72f 100644 --- a/src/connection/index.js +++ b/src/connection/index.js @@ -4,6 +4,7 @@ const FSM = require('fsm-event') const setImmediate = require('async/setImmediate') const Circuit = require('libp2p-circuit') const multistream = require('multistream-select') +const withIs = require('class-is') const BaseConnection = require('./base') const observeConnection = require('../observe-connection') @@ -24,17 +25,6 @@ const Errors = require('../errors') * coalescing dials and dial locks. */ class ConnectionFSM extends BaseConnection { - /** - * Determines if the given connection is an instance of ConnectionFSM - * - * @static - * @param {*} connection - * @returns {boolean} - */ - static isConnection (connection) { - return connection instanceof ConnectionFSM - } - /** * @param {ConnectionOptions} param0 * @constructor @@ -467,4 +457,7 @@ class ConnectionFSM extends BaseConnection { } } -module.exports = ConnectionFSM +module.exports = withIs(ConnectionFSM, { + className: 'ConnectionFSM', + symbolName: 'libp2p-switch/ConnectionFSM', +}) diff --git a/src/dialer.js b/src/dialer.js index aa52252..02597ae 100644 --- a/src/dialer.js +++ b/src/dialer.js @@ -55,7 +55,7 @@ function dial (_switch) { let connection = _switch.muxedConns[b58Id] || _switch.conns[b58Id] - if (!ConnectionFSM.isConnection(connection)) { + if (!ConnectionFSM.isConnectionFSM(connection)) { connection = new ConnectionFSM({ _switch, peerInfo, From 674d55c75f67db4e4a171073cfb3516d2ef85aa3 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Thu, 27 Sep 2018 10:13:50 +0200 Subject: [PATCH 12/27] refactor: clean up some logic and make inc muxed conns FSMs --- src/connection/base.js | 12 ++++++++++- src/connection/handler.js | 4 ---- src/connection/incoming.js | 7 +++---- src/connection/index.js | 19 ++++++------------ src/connection/manager.js | 7 ++++++- src/dialer.js | 41 +++++++++++++++++++------------------- 6 files changed, 46 insertions(+), 44 deletions(-) diff --git a/src/connection/base.js b/src/connection/base.js index fb224fa..fd4a90a 100644 --- a/src/connection/base.js +++ b/src/connection/base.js @@ -40,6 +40,16 @@ class BaseConnection extends EventEmitter { this._state('upgrade') } + /** + * Event handler for disconneced. + * + * @returns {void} + */ + _onDisconnected () { + this.log(`disconnected from ${this.theirB58Id}`) + this.removeAllListeners() + } + /** * Wraps this.conn with the Switch.protector for private connections * @@ -67,5 +77,5 @@ class BaseConnection extends EventEmitter { module.exports = withIs(BaseConnection, { className: 'BaseConnection', - symbolName: 'libp2p-switch/BaseConnection', + symbolName: 'libp2p-switch/BaseConnection' }) diff --git a/src/connection/handler.js b/src/connection/handler.js index c4c6fd8..9443167 100644 --- a/src/connection/handler.js +++ b/src/connection/handler.js @@ -1,10 +1,6 @@ 'use strict' -const FSM = require('fsm-event') const debug = require('debug') -const multistream = require('multistream-select') -const withIs = require('class-is') - const IncomingConnection = require('./incoming') const observeConn = require('../observe-connection') diff --git a/src/connection/incoming.js b/src/connection/incoming.js index a1c15b4..9c1ed28 100644 --- a/src/connection/incoming.js +++ b/src/connection/incoming.js @@ -1,11 +1,9 @@ 'use strict' const FSM = require('fsm-event') -const debug = require('debug') const multistream = require('multistream-select') const withIs = require('class-is') -const observeConn = require('../observe-connection') const BaseConnection = require('./base') class IncomingConnectionFSM extends BaseConnection { @@ -69,6 +67,7 @@ class IncomingConnectionFSM extends BaseConnection { if (this.theirPeerInfo) { this.theirPeerInfo.disconnect() } + this._state('done') }) } @@ -120,5 +119,5 @@ class IncomingConnectionFSM extends BaseConnection { module.exports = withIs(IncomingConnectionFSM, { className: 'IncomingConnectionFSM', - symbolName: 'libp2p-switch/IncomingConnectionFSM', -}) \ No newline at end of file + symbolName: 'libp2p-switch/IncomingConnectionFSM' +}) diff --git a/src/connection/index.js b/src/connection/index.js index 87cf72f..4f7ac01 100644 --- a/src/connection/index.js +++ b/src/connection/index.js @@ -43,10 +43,12 @@ class ConnectionFSM extends BaseConnection { // TODO: If given a muxer, we need to set the state // at connected. - // * A muxed connection should be fully connected. - // * A protocol handshake should generate a new connection + let startState = 'DISCONNECTED' + if (this.muxer) { + startState = 'MUXED' + } - this._state = FSM('DISCONNECTED', { + this._state = FSM(startState, { DISCONNECTED: { // No active connections exist for the peer dial: 'DIALING' }, @@ -254,15 +256,6 @@ class ConnectionFSM extends BaseConnection { this.emit('connected', this.conn) } - /** - * Event handler for disconneced. - * - * @returns {void} - */ - _onDisconnected () { - this.log(`disconnected from ${this.theirB58Id}`) - } - /** * Event handler for disconnecting. Handles any needed cleanup * @@ -459,5 +452,5 @@ class ConnectionFSM extends BaseConnection { module.exports = withIs(ConnectionFSM, { className: 'ConnectionFSM', - symbolName: 'libp2p-switch/ConnectionFSM', + symbolName: 'libp2p-switch/ConnectionFSM' }) diff --git a/src/connection/manager.js b/src/connection/manager.js index 357c7f6..161b3ed 100644 --- a/src/connection/manager.js +++ b/src/connection/manager.js @@ -7,6 +7,7 @@ const debug = require('debug') const log = debug('libp2p:switch:conn-manager') const once = require('once') const setImmediate = require('async/setImmediate') +const ConnectionFSM = require('../connection') const Circuit = require('libp2p-circuit') @@ -89,7 +90,11 @@ class ConnectionManager { } const b58Str = peerInfo.id.toB58String() - this.switch.muxedConns[b58Str] = { muxer: muxedConn } + this.switch.muxedConns[b58Str] = new ConnectionFSM({ + _switch: this.switch, + peerInfo, + muxer: muxedConn + }) if (peerInfo.multiaddrs.size > 0) { // with incomming conn and through identify, going to pick one diff --git a/src/dialer.js b/src/dialer.js index 02597ae..7cc88b7 100644 --- a/src/dialer.js +++ b/src/dialer.js @@ -61,32 +61,31 @@ function dial (_switch) { peerInfo, muxer: _switch.muxedConns[b58Id] || null }) + connection.once('error', (err) => callback(err)) + connection.on('connected', () => connection.protect()) + connection.on('private', () => connection.encrypt()) + connection.on('encrypted', () => connection.upgrade()) + connection.on('muxed', () => { + maybePerformHandshake({ + protocol, + proxyConnection, + connection, + callback + }) + }) + connection.on('unmuxed', () => { + maybePerformHandshake({ + protocol, + proxyConnection, + connection, + callback + }) + }) } const proxyConnection = new Connection() proxyConnection.setPeerInfo(peerInfo) - connection.once('error', (err) => callback(err)) - connection.once('connected', () => connection.protect()) - connection.once('private', () => connection.encrypt()) - connection.once('encrypted', () => connection.upgrade()) - connection.once('muxed', () => { - maybePerformHandshake({ - protocol, - proxyConnection, - connection, - callback - }) - }) - connection.once('unmuxed', () => { - maybePerformHandshake({ - protocol, - proxyConnection, - connection, - callback - }) - }) - // If we have a connection, maybe perform the protocol handshake // TODO: The basic connection probably shouldnt be reused const state = connection.getState() From f4a1806852fddfba1a611c1cd3cd408290191cda Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Thu, 27 Sep 2018 11:50:48 +0200 Subject: [PATCH 13/27] fix: cleanup todos, logic and event handlers --- src/connection/base.js | 9 +++++++++ src/connection/incoming.js | 2 +- src/connection/index.js | 17 ----------------- src/dialer.js | 16 +++++++--------- src/limit-dialer/queue.js | 6 ++++-- src/protocol-muxer.js | 1 - 6 files changed, 21 insertions(+), 30 deletions(-) diff --git a/src/connection/base.js b/src/connection/base.js index fd4a90a..470e527 100644 --- a/src/connection/base.js +++ b/src/connection/base.js @@ -13,6 +13,15 @@ class BaseConnection extends EventEmitter { this.log = debug(`libp2p:conn:${name}`) } + /** + * Gets the current state of the connection + * + * @returns {string} The current state of the connection + */ + getState () { + return this._state._state + } + /** * Puts the state into encrypting mode * diff --git a/src/connection/incoming.js b/src/connection/incoming.js index 9c1ed28..aeaf9e3 100644 --- a/src/connection/incoming.js +++ b/src/connection/incoming.js @@ -97,7 +97,7 @@ class IncomingConnectionFSM extends BaseConnection { }) }, null) - // Start handling the connection, this is only needed once + // Start handling the connection this.msListener.handle(this.conn, (err) => { if (err) { this.emit('crypto handshaking failed', err) diff --git a/src/connection/index.js b/src/connection/index.js index 4f7ac01..565b60c 100644 --- a/src/connection/index.js +++ b/src/connection/index.js @@ -41,8 +41,6 @@ class ConnectionFSM extends BaseConnection { this.conn = null // The base connection this.muxer = muxer // The upgraded/muxed connection - // TODO: If given a muxer, we need to set the state - // at connected. let startState = 'DISCONNECTED' if (this.muxer) { startState = 'MUXED' @@ -125,15 +123,6 @@ class ConnectionFSM extends BaseConnection { this._state.on('error', (err) => this._onStateError(err)) } - /** - * Gets the current state of the connection - * - * @returns {string} The current state of the connection - */ - getState () { - return this._state._state - } - /** * Puts the state into dialing mode * @@ -193,8 +182,6 @@ class ConnectionFSM extends BaseConnection { * @returns {void} */ _onDialing () { - // TODO: Start the connection flow - // TODO: Allow multiple dials? this.log(`dialing ${this.theirB58Id}`) if (!this.switch.hasTransports()) { @@ -269,10 +256,6 @@ class ConnectionFSM extends BaseConnection { this.theirPeerInfo.disconnect() this.log(`closed connection to ${this.theirB58Id}`) } - // TODO: should we do this? - if (this.ourPeerInfo) { - this.ourPeerInfo.disconnect() - } // Clean up stored connections if (this.muxer) { diff --git a/src/dialer.js b/src/dialer.js index 7cc88b7..e14d1ac 100644 --- a/src/dialer.js +++ b/src/dialer.js @@ -62,10 +62,10 @@ function dial (_switch) { muxer: _switch.muxedConns[b58Id] || null }) connection.once('error', (err) => callback(err)) - connection.on('connected', () => connection.protect()) - connection.on('private', () => connection.encrypt()) - connection.on('encrypted', () => connection.upgrade()) - connection.on('muxed', () => { + connection.once('connected', () => connection.protect()) + connection.once('private', () => connection.encrypt()) + connection.once('encrypted', () => connection.upgrade()) + connection.once('muxed', () => { maybePerformHandshake({ protocol, proxyConnection, @@ -73,7 +73,7 @@ function dial (_switch) { callback }) }) - connection.on('unmuxed', () => { + connection.once('unmuxed', () => { maybePerformHandshake({ protocol, proxyConnection, @@ -86,10 +86,8 @@ function dial (_switch) { const proxyConnection = new Connection() proxyConnection.setPeerInfo(peerInfo) - // If we have a connection, maybe perform the protocol handshake - // TODO: The basic connection probably shouldnt be reused - const state = connection.getState() - if (state === 'CONNECTED' || state === 'MUXED') { + // If we have a muxed connection, attempt the protocol handshake + if (connection.getState() === 'MUXED') { maybePerformHandshake({ protocol, proxyConnection, diff --git a/src/limit-dialer/queue.js b/src/limit-dialer/queue.js index c883589..8fff439 100644 --- a/src/limit-dialer/queue.js +++ b/src/limit-dialer/queue.js @@ -48,8 +48,10 @@ class DialQueue { log(`${transport.constructor.name}:work:cancel`) // clean up already done dials pull(pull.empty(), conn) - // TODO: proper cleanup once the connection interface supports it - // return conn.close(() => callback(new Error('Manual cancel')) + // If we can close the connection, do it + if (typeof conn.close === 'function') { + return conn.close((_) => callback(null, {cancel: true})) + } return callback(null, {cancel: true}) } diff --git a/src/protocol-muxer.js b/src/protocol-muxer.js index aed9fda..39c4a63 100644 --- a/src/protocol-muxer.js +++ b/src/protocol-muxer.js @@ -40,7 +40,6 @@ module.exports = function protocolMuxer (protocols, observer) { }) ms.handle(parentConn, (err) => { - // TODO: handle successful and failed connections for the FSM if (err) { log.error(`multistream handshake failed`, err) } From dd12ad1cb9d0e4a7dbfdcac9f2798cbebf5224fa Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Tue, 2 Oct 2018 10:58:22 +0200 Subject: [PATCH 14/27] refactor: clean up logs --- src/dialer.js | 2 +- src/index.js | 10 ++++++++-- src/limit-dialer/queue.js | 3 ++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/dialer.js b/src/dialer.js index e14d1ac..036e9f6 100644 --- a/src/dialer.js +++ b/src/dialer.js @@ -51,7 +51,7 @@ function dial (_switch) { const peerInfo = getPeerInfo(peer, _switch._peerBook) const b58Id = peerInfo.id.toB58String() - log(`${_switch._peerInfo.id.toB58String().slice(0, 8)} dial request to ${b58Id.slice(0, 8)} with protocol ${protocol}`) + log(`dialing to ${b58Id.slice(0, 8)} with protocol ${protocol || 'unknown'}`) let connection = _switch.muxedConns[b58Id] || _switch.conns[b58Id] diff --git a/src/index.js b/src/index.js index 4ea63c4..e0b6641 100644 --- a/src/index.js +++ b/src/index.js @@ -17,6 +17,7 @@ const assert = require('assert') const Errors = require('./errors') const debug = require('debug') const log = debug('libp2p:switch') +log.error = debug('libp2p:switch:error') /** * @fires Switch#stopped Triggered when the switch has stopped @@ -115,7 +116,10 @@ class Switch extends EventEmitter { log('The switch has stopped') this.emit('stopped') }) - this.state.on('error', (err) => this.emit('error', err)) + this.state.on('error', (err) => { + log.error(err) + this.emit('error', err) + }) } /** @@ -167,7 +171,7 @@ class Switch extends EventEmitter { /** * If a muxed Connection exists for the given peer, it will be closed - * and its reference on the Switch and will be removed. + * and its reference on the Switch will be removed. * * @param {PeerInfo|Multiaddr|PeerId} peer * @param {function()} callback @@ -239,6 +243,7 @@ class Switch extends EventEmitter { this.transport.listen(ts, {}, null, cb) }, (err) => { if (err) { + log.error(err) return this.emit('error', err) } this.state('done') @@ -264,6 +269,7 @@ class Switch extends EventEmitter { conn.muxer.end((err) => { // If OK things are fine, and someone just shut down if (err && err.message !== 'Fatal error: OK') { + log.error(err) return cb(err) } cb() diff --git a/src/limit-dialer/queue.js b/src/limit-dialer/queue.js index 8fff439..c204b62 100644 --- a/src/limit-dialer/queue.js +++ b/src/limit-dialer/queue.js @@ -7,6 +7,7 @@ const queue = require('async/queue') const debug = require('debug') const log = debug('libp2p:switch:dialer:queue') +log.error = debug('libp2p:switch:dialer:queue:error') /** * Queue up the amount of dials to a given peer. @@ -40,7 +41,7 @@ class DialQueue { log(`${transport.constructor.name}:work:start`) this._dialWithTimeout(transport, addr, (err, conn) => { if (err) { - log(`${transport.constructor.name}:work:error`, err) + log.error(`${transport.constructor.name}:work`, err) return callback(null, {error: err}) } From 56ea400f4b05ee45a0bdde8b8d1c0efca07826e0 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 3 Oct 2018 17:44:28 +0200 Subject: [PATCH 15/27] feat: add dialFSM to the switch --- src/connection/base.js | 1 + src/connection/index.js | 18 +++++-- src/dialer.js | 29 +++++----- src/errors.js | 1 + src/index.js | 1 + test/dial-emitter.node.js | 109 ++++++++++++++++++++++++++++++++++++++ test/node.js | 1 + 7 files changed, 144 insertions(+), 16 deletions(-) create mode 100644 test/dial-emitter.node.js diff --git a/src/connection/base.js b/src/connection/base.js index 470e527..8c4e265 100644 --- a/src/connection/base.js +++ b/src/connection/base.js @@ -56,6 +56,7 @@ class BaseConnection extends EventEmitter { */ _onDisconnected () { this.log(`disconnected from ${this.theirB58Id}`) + this.emit('close') this.removeAllListeners() } diff --git a/src/connection/index.js b/src/connection/index.js index 565b60c..5a76285 100644 --- a/src/connection/index.js +++ b/src/connection/index.js @@ -123,6 +123,15 @@ class ConnectionFSM extends BaseConnection { this._state.on('error', (err) => this._onStateError(err)) } + /** + * Puts the state into its disconnecting flow + * + * @returns {void} + */ + close () { + this._state('disconnect') + } + /** * Puts the state into dialing mode * @@ -198,14 +207,16 @@ class ConnectionFSM extends BaseConnection { let transport = key if (!transport) { if (!circuitEnabled) { - this.emit('error', new Error( - `Circuit not enabled and all transports failed to dial peer ${this.theirB58Id}!` + this.emit('error', Errors.CONNECTION_FAILED( + new Error(`Circuit not enabled and all transports failed to dial peer ${this.theirB58Id}!`) )) return this._state('disconnect') } if (circuitTried) { - this.emit('error', new Error(`No available transports to dial peer ${this.theirB58Id}!`)) + this.emit('error', Errors.CONNECTION_FAILED( + new Error(`No available transports to dial peer ${this.theirB58Id}!`) + )) return this._state('disconnect') } @@ -218,6 +229,7 @@ class ConnectionFSM extends BaseConnection { this.log(`dialing transport ${transport}`) this.switch.transport.dial(transport, this.theirPeerInfo, (err, _conn) => { if (err) { + this.emit('error:connection_attempt_failed', err.errors || [err]) this.log(err) return nextTransport(tKeys.shift()) } diff --git a/src/dialer.js b/src/dialer.js index 036e9f6..1a60cce 100644 --- a/src/dialer.js +++ b/src/dialer.js @@ -29,9 +29,10 @@ function maybePerformHandshake ({ protocol, proxyConnection, connection, callbac * to the given `peer`. * * @param {Switch} _switch + * @param {Boolean} returnFSM Whether or not to return an fsm instead of a Connection * @returns {function(PeerInfo, string, function(Error, Connection))} */ -function dial (_switch) { +function dial (_switch, returnFSM) { /** * Creates a new dialer and immediately begins dialing to the given `peer` * @@ -86,19 +87,21 @@ function dial (_switch) { const proxyConnection = new Connection() proxyConnection.setPeerInfo(peerInfo) - // If we have a muxed connection, attempt the protocol handshake - if (connection.getState() === 'MUXED') { - maybePerformHandshake({ - protocol, - proxyConnection, - connection, - callback - }) - } else { - connection.dial() - } + setImmediate(() => { + // If we have a muxed connection, attempt the protocol handshake + if (connection.getState() === 'MUXED') { + maybePerformHandshake({ + protocol, + proxyConnection, + connection, + callback + }) + } else { + connection.dial() + } + }) - return proxyConnection + return returnFSM ? connection : proxyConnection } } diff --git a/src/errors.js b/src/errors.js index 192a23f..a523bb9 100644 --- a/src/errors.js +++ b/src/errors.js @@ -3,6 +3,7 @@ const errCode = require('err-code') module.exports.PROTECTOR_REQUIRED = 'No protector provided with private network enforced' +module.exports.CONNECTION_FAILED = (err) => errCode(err, 'CONNECTION_FAILED') module.exports.DIAL_SELF = () => errCode(new Error('A node cannot dial itself'), 'DIAL_SELF') module.exports.NO_TRANSPORTS_REGISTERED = () => errCode(new Error('No transports registered, dial not possible'), 'NO_TRANSPORTS_REGISTERED') module.exports.UNEXPECTED_END = () => errCode(new Error('Unexpected end of input from reader.'), 'UNEXPECTED_END') diff --git a/src/index.js b/src/index.js index e0b6641..1698845 100644 --- a/src/index.js +++ b/src/index.js @@ -80,6 +80,7 @@ class Switch extends EventEmitter { // higher level (public) API this.dial = dial(this) + this.dialFSM = dial(this, true) // All purpose connection handler for managing incoming connections this._connectionHandler = connectionHandler(this) diff --git a/test/dial-emitter.node.js b/test/dial-emitter.node.js new file mode 100644 index 0000000..8955351 --- /dev/null +++ b/test/dial-emitter.node.js @@ -0,0 +1,109 @@ +/* eslint-env mocha */ +'use strict' + +const chai = require('chai') +const dirtyChai = require('dirty-chai') +const expect = chai.expect +chai.use(dirtyChai) +const PeerBook = require('peer-book') +const parallel = require('async/parallel') +const WS = require('libp2p-websockets') +const TCP = require('libp2p-tcp') +const secio = require('libp2p-secio') +const multiplex = require('libp2p-mplex') + +const utils = require('./utils') +const createInfos = utils.createInfos +const Switch = require('../src') + +describe('dialFSM', () => { + let switchA + let switchB + let switchC + + before((done) => createInfos(3, (err, infos) => { + expect(err).to.not.exist() + + const peerA = infos[0] + const peerB = infos[1] + const peerC = infos[2] + + peerA.multiaddrs.add('/ip4/0.0.0.0/tcp/0') + peerB.multiaddrs.add('/ip4/0.0.0.0/tcp/0') + peerC.multiaddrs.add('/ip4/0.0.0.0/tcp/0/ws') + // Give peer C a tcp address we wont actually support + peerC.multiaddrs.add('/ip4/0.0.0.0/tcp/0') + + switchA = new Switch(peerA, new PeerBook()) + switchB = new Switch(peerB, new PeerBook()) + switchC = new Switch(peerC, new PeerBook()) + + switchA.transport.add('tcp', new TCP()) + switchB.transport.add('tcp', new TCP()) + switchC.transport.add('ws', new WS()) + + switchA.connection.crypto(secio.tag, secio.encrypt) + switchB.connection.crypto(secio.tag, secio.encrypt) + switchC.connection.crypto(secio.tag, secio.encrypt) + + switchA.connection.addStreamMuxer(multiplex) + switchB.connection.addStreamMuxer(multiplex) + switchC.connection.addStreamMuxer(multiplex) + + parallel([ + (cb) => switchA.transport.listen('tcp', {}, null, cb), + (cb) => switchB.transport.listen('tcp', {}, null, cb), + (cb) => switchC.transport.listen('ws', {}, null, cb) + ], done) + })) + + after((done) => { + parallel([ + (cb) => switchA.stop(cb), + (cb) => switchB.stop(cb), + (cb) => switchC.stop(cb) + ], done) + }) + + it('should emit `error:connection_attempt_failed` when a transport fails to dial', (done) => { + switchC.handle('/warn/1.0.0', () => { }) + + const connFSM = switchA.dialFSM(switchC._peerInfo, '/warn/1.0.0', () => { }) + + connFSM.once('error:connection_attempt_failed', (errors) => { + expect(errors).to.be.an('array') + expect(errors).to.have.length(1) + expect(errors[0]).to.have.property('code', 'EADDRNOTAVAIL') + done() + }) + }) + + it('should emit an `error` event when a it cannot dial a peer', (done) => { + switchC.handle('/error/1.0.0', () => { }) + + const connFSM = switchA.dialFSM(switchC._peerInfo, '/error/1.0.0', () => { }) + + connFSM.once('error', (err) => { + expect(err).to.be.exist() + expect(err).to.have.property('code', 'CONNECTION_FAILED') + done() + }) + }) + + it('should emit a `closed` event when closed', (done) => { + switchB.handle('/closed/1.0.0', () => { }) + + const connFSM = switchA.dialFSM(switchB._peerInfo, '/closed/1.0.0', (err) => { + expect(err).to.not.exist() + expect(switchA.muxedConns).to.have.property(switchB._peerInfo.id.toB58String()) + connFSM.close() + }) + + connFSM.once('close', () => { + expect(switchA.muxedConns).to.not.have.any.keys([ + switchB._peerInfo.id.toB58String() + ]) + done() + }) + }) +}) diff --git a/test/node.js b/test/node.js index 127a5b1..30f213c 100644 --- a/test/node.js +++ b/test/node.js @@ -1,6 +1,7 @@ 'use strict' require('./connection.node') +require('./dial-emitter.node') require('./pnet.node') require('./transports.node') require('./stream-muxers.node') From 2e1f7b55f7ea3f2a24d73a8ed9cd63c6523346a4 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 3 Oct 2018 18:01:24 +0200 Subject: [PATCH 16/27] refactor: rename test file --- test/{dial-emitter.node.js => dial-fsm.node.js} | 0 test/node.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename test/{dial-emitter.node.js => dial-fsm.node.js} (100%) diff --git a/test/dial-emitter.node.js b/test/dial-fsm.node.js similarity index 100% rename from test/dial-emitter.node.js rename to test/dial-fsm.node.js diff --git a/test/node.js b/test/node.js index 30f213c..534f7e0 100644 --- a/test/node.js +++ b/test/node.js @@ -1,7 +1,7 @@ 'use strict' require('./connection.node') -require('./dial-emitter.node') +require('./dial-fsm.node') require('./pnet.node') require('./transports.node') require('./stream-muxers.node') From 75f1370f2c1dba419e752e58816a9290361dcdc7 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Thu, 4 Oct 2018 00:20:52 +0200 Subject: [PATCH 17/27] feat: add better support for closing connections --- package.json | 1 + src/connection/index.js | 11 ++++++++--- src/index.js | 16 +++++----------- test/dial-fsm.node.js | 29 ++++++++++++++++++++++++++++- 4 files changed, 42 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 5e8a6c3..4ca5dab 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "devDependencies": { "aegir": "^15.1.0", "chai": "^4.1.2", + "chai-checkmark": "^1.0.1", "dirty-chai": "^2.0.1", "libp2p-mplex": "~0.8.2", "libp2p-pnet": "~0.1.0", diff --git a/src/connection/index.js b/src/connection/index.js index 5a76285..f8f9ab7 100644 --- a/src/connection/index.js +++ b/src/connection/index.js @@ -48,7 +48,8 @@ class ConnectionFSM extends BaseConnection { this._state = FSM(startState, { DISCONNECTED: { // No active connections exist for the peer - dial: 'DIALING' + dial: 'DIALING', + disconnect: 'DISCONNECTED' }, DIALING: { // Creating an initial connection abort: 'ABORTED', @@ -129,6 +130,7 @@ class ConnectionFSM extends BaseConnection { * @returns {void} */ close () { + this.log(`closing connection to ${this.theirB58Id}`) this._state('disconnect') } @@ -266,19 +268,21 @@ class ConnectionFSM extends BaseConnection { // Issue disconnects on both Peers if (this.theirPeerInfo) { this.theirPeerInfo.disconnect() - this.log(`closed connection to ${this.theirB58Id}`) } // Clean up stored connections if (this.muxer) { - setImmediate(() => this.switch.emit('peer-mux-closed', this.theirPeerInfo)) + this.muxer.end() } + delete this.switch.muxedConns[this.theirB58Id] delete this.switch.conns[this.theirB58Id] delete this.muxer delete this.conn this._state('done') + + setImmediate(() => this.switch.emit('peer-mux-closed', this.theirPeerInfo)) } /** @@ -363,6 +367,7 @@ class ConnectionFSM extends BaseConnection { this.switch.muxedConns[this.theirB58Id] = this this.muxer.once('close', () => { + delete this.muxer this._state('disconnect') }) diff --git a/src/index.js b/src/index.js index 1698845..438acb6 100644 --- a/src/index.js +++ b/src/index.js @@ -182,12 +182,12 @@ class Switch extends EventEmitter { const peerInfo = getPeerInfo(peer, this.peerBook) const key = peerInfo.id.toB58String() if (this.muxedConns[key]) { - const muxer = this.muxedConns[key].muxer - muxer.once('close', () => { + const conn = this.muxedConns[key] + conn.once('close', () => { delete this.muxedConns[key] callback() }) - muxer.end() + conn.close() } else { callback() } @@ -267,14 +267,8 @@ class Switch extends EventEmitter { return cb() } - conn.muxer.end((err) => { - // If OK things are fine, and someone just shut down - if (err && err.message !== 'Fatal error: OK') { - log.error(err) - return cb(err) - } - cb() - }) + conn.once('close', cb) + conn.close() }, cb), (cb) => { each(this.transports, (transport, cb) => { diff --git a/test/dial-fsm.node.js b/test/dial-fsm.node.js index 8955351..8c188b5 100644 --- a/test/dial-fsm.node.js +++ b/test/dial-fsm.node.js @@ -4,6 +4,7 @@ const chai = require('chai') const dirtyChai = require('dirty-chai') const expect = chai.expect +chai.use(require('chai-checkmark')) chai.use(dirtyChai) const PeerBook = require('peer-book') const parallel = require('async/parallel') @@ -50,6 +51,10 @@ describe('dialFSM', () => { switchB.connection.addStreamMuxer(multiplex) switchC.connection.addStreamMuxer(multiplex) + switchA.connection.reuse() + switchB.connection.reuse() + switchC.connection.reuse() + parallel([ (cb) => switchA.transport.listen('tcp', {}, null, cb), (cb) => switchB.transport.listen('tcp', {}, null, cb), @@ -73,7 +78,6 @@ describe('dialFSM', () => { connFSM.once('error:connection_attempt_failed', (errors) => { expect(errors).to.be.an('array') expect(errors).to.have.length(1) - expect(errors[0]).to.have.property('code', 'EADDRNOTAVAIL') done() }) }) @@ -106,4 +110,27 @@ describe('dialFSM', () => { done() }) }) + + it('should close when the receiver closes', (done) => { + const peerIdA = switchA._peerInfo.id.toB58String() + + // wait for the expects to happen + expect(2).checks(done) + + switchB.handle('/closed/1.0.0', () => { }) + switchB.on('peer-mux-established', (peerInfo) => { + if (peerInfo.id.toB58String() === peerIdA) { + switchB.removeAllListeners('peer-mux-established') + expect(switchB.muxedConns).to.have.property(peerIdA).mark() + switchB.muxedConns[peerIdA].close() + } + }) + + const connFSM = switchA.dialFSM(switchB._peerInfo, '/closed/1.0.0', () => { }) + connFSM.once('close', () => { + expect(switchA.muxedConns).to.not.have.any.keys([ + switchB._peerInfo.id.toB58String() + ]).mark() + }) + }) }) From 0ee8157cc5fbb504d1c6d93e85d776e9dc563dfb Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Thu, 4 Oct 2018 00:49:40 +0200 Subject: [PATCH 18/27] test: add tests for some uncovered lines --- test/connection.node.js | 31 +++++++++++++++++++++++++++++++ test/transports.node.js | 9 +++++++++ 2 files changed, 40 insertions(+) diff --git a/test/connection.node.js b/test/connection.node.js index 8902e3f..cf04073 100644 --- a/test/connection.node.js +++ b/test/connection.node.js @@ -156,6 +156,37 @@ describe('ConnectionFSM', () => { connection.dial() }) + it('should not return a connection when handshaking with no protocol', (done) => { + const connection = new ConnectionFSM({ + _switch: dialerSwitch, + peerInfo: listenerSwitch._peerInfo + }) + + listenerSwitch.handle('/muxed-conn-test/1.0.0', (_, conn) => { + return pull(conn, conn) + }) + + connection.once('connected', (conn) => { + expect(conn).to.be.an.instanceof(Connection) + connection.encrypt() + }) + connection.once('encrypted', (conn) => { + expect(conn).to.be.an.instanceof(Connection) + connection.upgrade() + }) + connection.once('muxed', (conn) => { + expect(conn.multicodec).to.equal(multiplex.multicodec) + + connection.shake(null, (err, protocolConn) => { + expect(err).to.not.exist() + expect(protocolConn).to.not.exist() + done() + }) + }) + + connection.dial() + }) + describe('with no muxers', () => { let oldMuxers before(() => { diff --git a/test/transports.node.js b/test/transports.node.js index e0ed175..f815124 100644 --- a/test/transports.node.js +++ b/test/transports.node.js @@ -52,6 +52,15 @@ describe('transports', () => { expect(Object.keys(switchB.transports).length).to.equal(1) }) + it('.transport.remove', () => { + switchA.transport.add('test', new WS()) + expect(switchA.transports).to.have.any.keys(['test']) + switchA.transport.remove('test') + expect(switchA.transports).to.not.have.any.keys(['test']) + // verify remove fails silently + switchA.transport.remove('test') + }) + it('.transport.listen', (done) => { let count = 0 From d921c2d6a1737d882f5ba753dfea11db8a90226c Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Thu, 4 Oct 2018 11:52:46 +0200 Subject: [PATCH 19/27] refactor: do some cleanup --- src/connection/base.js | 14 +++++++++++++- src/connection/incoming.js | 16 ++++------------ src/connection/index.js | 2 +- src/connection/manager.js | 8 -------- 4 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/connection/base.js b/src/connection/base.js index 8c4e265..485bd7d 100644 --- a/src/connection/base.js +++ b/src/connection/base.js @@ -50,8 +50,9 @@ class BaseConnection extends EventEmitter { } /** - * Event handler for disconneced. + * Event handler for disconnected. * + * @fires BaseConnection#close * @returns {void} */ _onDisconnected () { @@ -60,6 +61,17 @@ class BaseConnection extends EventEmitter { this.removeAllListeners() } + /** + * Event handler for privatized + * + * @fires BaseConnection#private + * @returns {void} + */ + _onPrivatized () { + this.log(`successfully privatized incoming connection`) + this.emit('private', this.conn) + } + /** * Wraps this.conn with the Switch.protector for private connections * diff --git a/src/connection/incoming.js b/src/connection/incoming.js index aeaf9e3..21c0e12 100644 --- a/src/connection/incoming.js +++ b/src/connection/incoming.js @@ -72,15 +72,12 @@ class IncomingConnectionFSM extends BaseConnection { } /** - * Gets the current state of the connection + * Attempts to encrypt `this.conn` with the Switch's crypto. * - * @returns {string} The current state of the connection + * @private + * @fires IncomingConnectionFSM#error + * @returns {void} */ - getState () { - return this._state._state - } - - // TODO: We need to handle N+1 crypto libraries _onEncrypting () { this.log(`encrypting connection via ${this.switch.crypto.tag}`) @@ -105,11 +102,6 @@ class IncomingConnectionFSM extends BaseConnection { }) } - _onPrivatized () { - this.log(`successfully privatized incoming connection`) - this.emit('private', this.conn) - } - _onUpgrading () { this.log('adding the protocol muxer to the connection') this.protocolMuxer(this.conn, this.msListener) diff --git a/src/connection/index.js b/src/connection/index.js index f8f9ab7..f246cb1 100644 --- a/src/connection/index.js +++ b/src/connection/index.js @@ -103,7 +103,7 @@ class ConnectionFSM extends BaseConnection { this._state.on('DIALING', () => this._onDialing()) this._state.on('DIALED', () => this._onDialed()) this._state.on('PRIVATIZING', () => this._onPrivatizing()) - this._state.on('PRIVATIZED', () => this.emit('private', this.conn)) + this._state.on('PRIVATIZED', () => this._onPrivatized()) this._state.on('ENCRYPTING', () => this._onEncrypting()) this._state.on('ENCRYPTED', () => { this.log(`successfully encrypted connection to ${this.theirB58Id}`) diff --git a/src/connection/manager.js b/src/connection/manager.js index 161b3ed..024c187 100644 --- a/src/connection/manager.js +++ b/src/connection/manager.js @@ -140,14 +140,6 @@ class ConnectionManager { encrypt = plaintext.encrypt } - this.switch.unhandle(this.switch.crypto.tag) - this.switch.handle(tag, (protocol, conn) => { - const myId = this.switch._peerInfo.id - const secure = encrypt(myId, conn, undefined, () => { - this.switch.protocolMuxer(null)(secure) - }) - }) - this.switch.crypto = {tag, encrypt} } From 635136564414884d260d19727674df974d72673e Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Fri, 5 Oct 2018 17:01:39 +0200 Subject: [PATCH 20/27] feat: add additional fsm user support --- src/connection/index.js | 1 + src/index.js | 8 ++++---- src/transport.js | 16 ++++++++++++++++ test/transports.node.js | 28 +++++++++++++++++++--------- 4 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/connection/index.js b/src/connection/index.js index f246cb1..21b71e0 100644 --- a/src/connection/index.js +++ b/src/connection/index.js @@ -433,6 +433,7 @@ class ConnectionFSM extends BaseConnection { const conn = observeConnection(null, protocol, _conn, this.switch.observer) this.log(`successfully performed handshake of ${protocol} to ${this.theirB58Id}`) + this.emit('connection', conn) callback(null, conn) }) }) diff --git a/src/index.js b/src/index.js index 438acb6..939e018 100644 --- a/src/index.js +++ b/src/index.js @@ -111,11 +111,11 @@ class Switch extends EventEmitter { }) this.state.on('STARTED', () => { log('The switch has started') - this.emit('started') + this.emit('start') }) this.state.on('STOPPED', () => { log('The switch has stopped') - this.emit('stopped') + this.emit('stop') }) this.state.on('error', (err) => { log.error(err) @@ -212,7 +212,7 @@ class Switch extends EventEmitter { */ start (callback = () => {}) { // Add once listener for deprecated callback support - this.once('started', callback) + this.once('start', callback) this.state('start') } @@ -226,7 +226,7 @@ class Switch extends EventEmitter { */ stop (callback = () => {}) { // Add once listener for deprecated callback support - this.once('stopped', callback) + this.once('stop', callback) this.state('stop') } diff --git a/src/transport.js b/src/transport.js index 4f4bc7b..db11567 100644 --- a/src/transport.js +++ b/src/transport.js @@ -64,6 +64,22 @@ class TransportManager { }) } + /** + * Calls `remove` on each transport the switch has + * + * @param {function(Error)} callback + * @returns {void} + */ + removeAll (callback) { + const tasks = Object.keys(this.switch.transports).map((key) => { + return (cb) => { + this.remove(key, cb) + } + }) + + parallel(tasks, callback) + } + /** * For a given transport `key`, dial to all that transport multiaddrs * diff --git a/test/transports.node.js b/test/transports.node.js index f815124..98347f1 100644 --- a/test/transports.node.js +++ b/test/transports.node.js @@ -44,16 +44,8 @@ describe('transports', () => { }) }) - it('.transport.add', () => { - switchA.transport.add(t.n, new t.C()) - expect(Object.keys(switchA.transports).length).to.equal(1) - - switchB.transport.add(t.n, new t.C()) - expect(Object.keys(switchB.transports).length).to.equal(1) - }) - it('.transport.remove', () => { - switchA.transport.add('test', new WS()) + switchA.transport.add('test', new t.C()) expect(switchA.transports).to.have.any.keys(['test']) switchA.transport.remove('test') expect(switchA.transports).to.not.have.any.keys(['test']) @@ -61,6 +53,24 @@ describe('transports', () => { switchA.transport.remove('test') }) + it('.transport.removeAll', (done) => { + switchA.transport.add('test', new t.C()) + switchA.transport.add('test2', new t.C()) + expect(switchA.transports).to.have.any.keys(['test', 'test2']) + switchA.transport.removeAll(() => { + expect(switchA.transports).to.not.have.any.keys(['test', 'test2']) + done() + }) + }) + + it('.transport.add', () => { + switchA.transport.add(t.n, new t.C()) + expect(Object.keys(switchA.transports).length).to.equal(1) + + switchB.transport.add(t.n, new t.C()) + expect(Object.keys(switchB.transports).length).to.equal(1) + }) + it('.transport.listen', (done) => { let count = 0 From 3d469d0f862f0ee57da179ee822d1a59c9437d87 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Fri, 5 Oct 2018 17:33:48 +0200 Subject: [PATCH 21/27] feat: add warning emitter for muxer upgrade failed --- src/connection/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/connection/index.js b/src/connection/index.js index 21b71e0..8d31d98 100644 --- a/src/connection/index.js +++ b/src/connection/index.js @@ -399,6 +399,7 @@ class ConnectionFSM extends BaseConnection { if (err) { this.log('Error upgrading connection:', err) this.switch.conns[this.theirB58Id] = this + this.emit('error:upgrade_failed', err) // Cant upgrade, hold the encrypted connection return this._state('stop') } From 662a58fb64c62b294b4c30e901185cb3f37481da Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Fri, 5 Oct 2018 19:04:57 +0200 Subject: [PATCH 22/27] refactor: cleanup and add some tests --- src/connection/index.js | 8 +++++--- src/errors.js | 1 + src/index.js | 6 ------ test/connection.node.js | 15 +++++++++++++++ test/swarm-no-muxing.node.js | 2 +- 5 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/connection/index.js b/src/connection/index.js index 8d31d98..1a4ce20 100644 --- a/src/connection/index.js +++ b/src/connection/index.js @@ -49,7 +49,8 @@ class ConnectionFSM extends BaseConnection { this._state = FSM(startState, { DISCONNECTED: { // No active connections exist for the peer dial: 'DIALING', - disconnect: 'DISCONNECTED' + disconnect: 'DISCONNECTED', + done: 'DISCONNECTED' }, DIALING: { // Creating an initial connection abort: 'ABORTED', @@ -91,7 +92,8 @@ class ConnectionFSM extends BaseConnection { disconnect: 'DISCONNECTING' }, DISCONNECTING: { // Shutting down the connection - done: 'DISCONNECTED' + done: 'DISCONNECTED', + disconnect: 'DISCONNECTING' }, ABORTED: { }, // A severe event occurred ERRORED: { // An error occurred, but future dials may be allowed @@ -447,7 +449,7 @@ class ConnectionFSM extends BaseConnection { * @returns {void} */ _onStateError (err) { - // TODO: may need to do something for legit invalid transitions + this.emit('error', Errors.INVALID_STATE_TRANSITION(err)) this.log(err) } } diff --git a/src/errors.js b/src/errors.js index a523bb9..22833bd 100644 --- a/src/errors.js +++ b/src/errors.js @@ -7,6 +7,7 @@ module.exports.CONNECTION_FAILED = (err) => errCode(err, 'CONNECTION_FAILED') module.exports.DIAL_SELF = () => errCode(new Error('A node cannot dial itself'), 'DIAL_SELF') module.exports.NO_TRANSPORTS_REGISTERED = () => errCode(new Error('No transports registered, dial not possible'), 'NO_TRANSPORTS_REGISTERED') module.exports.UNEXPECTED_END = () => errCode(new Error('Unexpected end of input from reader.'), 'UNEXPECTED_END') +module.exports.INVALID_STATE_TRANSITION = (err) => errCode(err, 'INVALID_STATE_TRANSITION') module.exports.maybeUnexpectedEnd = (err) => { if (err === true) { diff --git a/src/index.js b/src/index.js index 939e018..e9f934a 100644 --- a/src/index.js +++ b/src/index.js @@ -72,12 +72,6 @@ class Switch extends EventEmitter { this.stats = Stats(this.observer, this._options.stats) this.protocolMuxer = ProtocolMuxer(this.protocols, this.observer) - this.handle(this.crypto.tag, (protocol, conn) => { - const peerId = this._peerInfo.id - const wrapped = this.crypto.encrypt(peerId, conn, undefined, () => {}) - return this.protocolMuxer(null)(wrapped) - }) - // higher level (public) API this.dial = dial(this) this.dialFSM = dial(this, true) diff --git a/test/connection.node.js b/test/connection.node.js index cf04073..f546541 100644 --- a/test/connection.node.js +++ b/test/connection.node.js @@ -71,6 +71,21 @@ describe('ConnectionFSM', () => { expect(connection.getState()).to.equal('DISCONNECTED') }) + it('should emit an error with an invalid transition', (done) => { + const connection = new ConnectionFSM({ + _switch: dialerSwitch, + peerInfo: listenerSwitch._peerInfo + }) + + expect(connection.getState()).to.equal('DISCONNECTED') + + connection.once('error', (err) => { + expect(err).to.have.property('code', 'INVALID_STATE_TRANSITION') + done() + }) + connection.upgrade() + }) + it('.dial should create a basic connection', (done) => { const connection = new ConnectionFSM({ _switch: dialerSwitch, diff --git a/test/swarm-no-muxing.node.js b/test/swarm-no-muxing.node.js index 77e3a5c..9fbda72 100644 --- a/test/swarm-no-muxing.node.js +++ b/test/swarm-no-muxing.node.js @@ -48,7 +48,7 @@ describe('Switch (no Stream Multiplexing)', () => { it('handle a protocol', (done) => { switchB.handle('/bananas/1.0.0', (protocol, conn) => pull(conn, conn)) - expect(Object.keys(switchB.protocols).length).to.equal(2) + expect(switchB.protocols).to.have.all.keys('/bananas/1.0.0') done() }) From 730c2b6a10eac5e7da8a9d20a049e1856721e1ba Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Fri, 5 Oct 2018 19:28:13 +0200 Subject: [PATCH 23/27] test: add test for failed muxer upgrade --- test/connection.node.js | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/test/connection.node.js b/test/connection.node.js index f546541..35cf0ce 100644 --- a/test/connection.node.js +++ b/test/connection.node.js @@ -11,6 +11,7 @@ const parallel = require('async/parallel') const secio = require('libp2p-secio') const pull = require('pull-stream') const multiplex = require('libp2p-mplex') +const spdy = require('libp2p-spdy') const Connection = require('interface-connection').Connection const Protector = require('libp2p-pnet') const generatePSK = Protector.generate @@ -23,11 +24,12 @@ const Switch = require('../src') const createInfos = require('./utils').createInfos describe('ConnectionFSM', () => { + let spdySwitch let listenerSwitch let dialerSwitch before((done) => { - createInfos(2, (err, infos) => { + createInfos(3, (err, infos) => { if (err) { return done(err) } @@ -44,9 +46,16 @@ describe('ConnectionFSM', () => { listenerSwitch.connection.addStreamMuxer(multiplex) listenerSwitch.transport.add('ws', new WS()) + spdySwitch = new Switch(infos.shift(), new PeerBook()) + spdySwitch._peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/15453/ws') + spdySwitch.connection.crypto(secio.tag, secio.encrypt) + spdySwitch.connection.addStreamMuxer(spdy) + spdySwitch.transport.add('ws', new WS()) + parallel([ (cb) => dialerSwitch.start(cb), - (cb) => listenerSwitch.start(cb) + (cb) => listenerSwitch.start(cb), + (cb) => spdySwitch.start(cb) ], (err) => { done(err) }) @@ -56,7 +65,8 @@ describe('ConnectionFSM', () => { after((done) => { parallel([ (cb) => dialerSwitch.stop(cb), - (cb) => listenerSwitch.stop(cb) + (cb) => listenerSwitch.stop(cb), + (cb) => spdySwitch.stop(cb) ], () => { done() }) @@ -140,6 +150,28 @@ describe('ConnectionFSM', () => { connection.dial() }) + it('should fail to upgrade a connection with incompatible muxers', (done) => { + const connection = new ConnectionFSM({ + _switch: dialerSwitch, + peerInfo: spdySwitch._peerInfo + }) + + connection.once('connected', (conn) => { + expect(conn).to.be.an.instanceof(Connection) + connection.encrypt() + }) + connection.once('encrypted', (conn) => { + expect(conn).to.be.an.instanceof(Connection) + connection.upgrade() + }) + connection.once('error:upgrade_failed', (err) => { + expect(err).to.exist() + done() + }) + + connection.dial() + }) + it('should be able to handshake a protocol over a muxed connection', (done) => { const connection = new ConnectionFSM({ _switch: dialerSwitch, From 6dd8f39376224b9addabcb22720c1ae95214ee66 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Fri, 5 Oct 2018 22:43:33 +0200 Subject: [PATCH 24/27] test: add more error state tests for connectionfsm --- src/connection/index.js | 8 +++--- test/connection.node.js | 55 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/src/connection/index.js b/src/connection/index.js index 1a4ce20..01a6ec9 100644 --- a/src/connection/index.js +++ b/src/connection/index.js @@ -306,16 +306,16 @@ class ConnectionFSM extends BaseConnection { msDialer.select(this.switch.crypto.tag, (err, _conn) => { if (err) { - this.emit('error', Errors.maybeUnexpectedEnd(err)) - return this._state('disconnect') + this._state('disconnect') + return this.emit('error', Errors.maybeUnexpectedEnd(err)) } const conn = observeConnection(null, this.switch.crypto.tag, _conn, this.switch.observer) this.conn = this.switch.crypto.encrypt(this.ourPeerInfo.id, conn, this.theirPeerInfo.id, (err) => { if (err) { - this.emit('error', err) - return this._state('disconnect') + this._state('disconnect') + return this.emit('error', err) } this.conn.setPeerInfo(this.theirPeerInfo) diff --git a/test/connection.node.js b/test/connection.node.js index 35cf0ce..1e1d100 100644 --- a/test/connection.node.js +++ b/test/connection.node.js @@ -2,9 +2,10 @@ 'use strict' const chai = require('chai') -const dirtyChai = require('dirty-chai') const expect = chai.expect -chai.use(dirtyChai) +chai.use(require('dirty-chai')) +chai.use(require('chai-checkmark')) +const sinon = require('sinon') const PeerBook = require('peer-book') const WS = require('libp2p-websockets') const parallel = require('async/parallel') @@ -110,6 +111,32 @@ describe('ConnectionFSM', () => { connection.dial() }) + it('should emit warning on dial failed attempt', (done) => { + const connection = new ConnectionFSM({ + _switch: dialerSwitch, + peerInfo: listenerSwitch._peerInfo + }) + + const stub = sinon.stub(dialerSwitch.transport, 'dial').callsArgWith(2, { + errors: [ + new Error('address in use') + ] + }) + + connection.once('error:connection_attempt_failed', (errors) => { + expect(errors).to.have.length(1).mark() + stub.restore() + }) + + connection.once('error', (err) => { + expect(err).to.exist().mark() + }) + + expect(2).checks(done) + + connection.dial() + }) + it('should be able to encrypt a basic connection', (done) => { const connection = new ConnectionFSM({ _switch: dialerSwitch, @@ -128,6 +155,30 @@ describe('ConnectionFSM', () => { connection.dial() }) + it('should disconnect on encryption failure', (done) => { + const connection = new ConnectionFSM({ + _switch: dialerSwitch, + peerInfo: listenerSwitch._peerInfo + }) + + const stub = sinon.stub(dialerSwitch.crypto, 'encrypt') + .callsArgWith(3, new Error('fail encrypt')) + + connection.once('connected', (conn) => { + expect(conn).to.be.an.instanceof(Connection) + connection.encrypt() + }) + connection.once('close', () => { + stub.restore() + done() + }) + connection.once('encrypted', () => { + throw new Error('should not encrypt') + }) + + connection.dial() + }) + it('should be able to upgrade an encrypted connection', (done) => { const connection = new ConnectionFSM({ _switch: dialerSwitch, From a2e995e919aef4a0843669a74f2a7d64b54b49b8 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 17 Oct 2018 22:51:32 +0200 Subject: [PATCH 25/27] docs: update readme --- README.md | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 11953dc..d5d9521 100644 --- a/README.md +++ b/README.md @@ -26,16 +26,17 @@ libp2p-switch is used by [libp2p](https://github.com/libp2p/js-libp2p) but it ca - [Usage](#usage) - [Create a libp2p switch](#create-a-libp2p-switch) - [API](#api) - - [`switch.dial(peer, protocol, callback)`](#swarmdialpi-protocol-callback) - - [`switch.hangUp(peer, callback)`](#swarmhanguppi-callback) - - [`switch.handle(protocol, handler)`](#swarmhandleprotocol-handler) - - [`switch.unhandle(protocol)`](#swarmunhandleprotocol) - - [`switch.start(callback)`](#swarmlistencallback) - - [`switch.stop(callback)`](#swarmclosecallback) - - [`switch.connection`](#connection) + - [`switch.dial(peer, protocol, callback)`](#switchdialpeer-protocol-callback) + - [`switch.dialFSM(peer, protocol, callback)`](#switchdialfsmpeer-protocol-callback) + - [`switch.hangUp(peer, callback)`](#switchhanguppeer-callback) + - [`switch.handle(protocol, handler)`](#switchhandleprotocol-handler) + - [`switch.unhandle(protocol)`](#switchunhandleprotocol) + - [`switch.start(callback)`](#switchstartcallback) + - [`switch.stop(callback)`](#switchstopcallback) + - [`switch.connection`](#switchconnection) - [`switch.stats`](#stats-api) - - [Internal Transports API](#transports) -- [Design Notes](#designnotes) + - [Internal Transports API](#internal-transports-api) +- [Design Notes](#design-notes) - [Multitransport](#multitransport) - [Connection upgrades](#connection-upgrades) - [Identify](#identify) @@ -94,6 +95,25 @@ dial uses the best transport (whatever works first, in the future we can have so - `protocol` - `callback` +### `switch.dialFSM(peer, protocol, callback)` + +works like dial, but calls back with a [Connection State Machine](#connection-state-machine) + +- `peer`: can be an instance of [PeerInfo][], [PeerId][] or [multiaddr][] +- `protocol`: String that defines the protocol (e.g '/ipfs/bitswap/1.1.0') to be used +- `callback`: Function with signature `function (err, connFSM) {}` where `connFSM` is a [Connection State Machine](#connection-state-machine) + +#### Connection State Machine +Connection state machines emit a number of events that can be used to determine the current state of the connection +and to received the underlying connection that can be used to transfer data. + +##### Events +- `error`: emitted whenever a fatal error occurs with the connection; the error will be emitted. +- `error:upgrade_failed`: emitted whenever the connection fails to upgrade with a muxer, this is not fatal. +- `error:connection_attempt_failed`: emitted whenever a dial attempt fails for a given transport. An array of errors is emitted. +- `connection`: emitted whenever a useable connection has been established; the underlying [Connection](https://github.com/libp2p/interface-connection) will be emitted. +- `close`: emitted when the connection has closed. + ### `switch.hangUp(peer, callback)` Hang up the muxed connection we have with the peer. From ef85443a3c8306889a97aba547d08c82de3f7471 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 17 Oct 2018 22:57:06 +0200 Subject: [PATCH 26/27] docs: fix readme link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d5d9521..b32fc29 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ libp2p-switch is used by [libp2p](https://github.com/libp2p/js-libp2p) but it ca - [`switch.dial(peer, protocol, callback)`](#switchdialpeer-protocol-callback) - [`switch.dialFSM(peer, protocol, callback)`](#switchdialfsmpeer-protocol-callback) - [`switch.hangUp(peer, callback)`](#switchhanguppeer-callback) - - [`switch.handle(protocol, handler)`](#switchhandleprotocol-handler) + - [`switch.handle(protocol, handlerFunc, matchFunc)`](#switchhandleprotocol-handlerfunc-matchfunc) - [`switch.unhandle(protocol)`](#switchunhandleprotocol) - [`switch.start(callback)`](#switchstartcallback) - [`switch.stop(callback)`](#switchstopcallback) From 8ff03752c56065cccf392dbad360ba0748d0c555 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Fri, 19 Oct 2018 13:03:44 +0200 Subject: [PATCH 27/27] docs: clean up readme and jsdocs --- README.md | 120 ++++++++++++++++++++++++++++----------------------- src/index.js | 10 ++--- 2 files changed, 69 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index b32fc29..3f78bb7 100644 --- a/README.md +++ b/README.md @@ -26,15 +26,15 @@ libp2p-switch is used by [libp2p](https://github.com/libp2p/js-libp2p) but it ca - [Usage](#usage) - [Create a libp2p switch](#create-a-libp2p-switch) - [API](#api) + - [`switch.connection`](#switchconnection) - [`switch.dial(peer, protocol, callback)`](#switchdialpeer-protocol-callback) - [`switch.dialFSM(peer, protocol, callback)`](#switchdialfsmpeer-protocol-callback) - - [`switch.hangUp(peer, callback)`](#switchhanguppeer-callback) - [`switch.handle(protocol, handlerFunc, matchFunc)`](#switchhandleprotocol-handlerfunc-matchfunc) - - [`switch.unhandle(protocol)`](#switchunhandleprotocol) + - [`switch.hangUp(peer, callback)`](#switchhanguppeer-callback) - [`switch.start(callback)`](#switchstartcallback) - [`switch.stop(callback)`](#switchstopcallback) - - [`switch.connection`](#switchconnection) - [`switch.stats`](#stats-api) + - [`switch.unhandle(protocol)`](#switchunhandleprotocol) - [Internal Transports API](#internal-transports-api) - [Design Notes](#design-notes) - [Multitransport](#multitransport) @@ -87,6 +87,46 @@ tests]([./test/pnet.node.js]). - peerInfo is a [PeerInfo](https://github.com/libp2p/js-peer-info) object that has the peer information. - peerBook is a [PeerBook](https://github.com/libp2p/js-peer-book) object that stores all the known peers. +### `switch.connection` + +##### `switch.connection.addUpgrade()` + +A connection upgrade must be able to receive and return something that implements the [interface-connection](https://github.com/libp2p/interface-connection) specification. + +> **WIP** + +##### `switch.connection.addStreamMuxer(muxer)` + +Upgrading a connection to use a stream muxer is still considered an upgrade, but a special case since once this connection is applied, the returned obj will implement the [interface-stream-muxer](https://github.com/libp2p/interface-stream-muxer) spec. + +- `muxer` + +##### `switch.connection.reuse()` + +Enable the identify protocol. + +##### `switch.connection.crypto([tag, encrypt])` + +Enable a specified crypto protocol. By default no encryption is used, aka `plaintext`. If called with no arguments it resets to use `plaintext`. + +You can use for example [libp2p-secio](https://github.com/libp2p/js-libp2p-secio) like this + +```js +const secio = require('libp2p-secio') +switch.connection.crypto(secio.tag, secio.encrypt) +``` + +##### `switch.connection.enableCircuitRelay(options, callback)` + +Enable circuit relaying. + +- `options` + - enabled - activates relay dialing and listening functionality + - hop - an object with two properties + - enabled - enables circuit relaying + - active - is it an active or passive relay (default false) +- `callback` + ### `switch.dial(peer, protocol, callback)` dial uses the best transport (whatever works first, in the future we can have some criteria), and jump starts the connection until the point where we have to negotiate the protocol. If a muxer is available, then drop the muxer onto that connection. Good to warm up connections or to check for connectivity. If we have already a muxer for that peerInfo, then do nothing. @@ -114,14 +154,6 @@ and to received the underlying connection that can be used to transfer data. - `connection`: emitted whenever a useable connection has been established; the underlying [Connection](https://github.com/libp2p/interface-connection) will be emitted. - `close`: emitted when the connection has closed. -### `switch.hangUp(peer, callback)` - -Hang up the muxed connection we have with the peer. - -- `peer`: can be an instance of [PeerInfo][], [PeerId][] or [multiaddr][] -- `callback` - - ### `switch.handle(protocol, handlerFunc, matchFunc)` Handle a new protocol. @@ -130,68 +162,43 @@ Handle a new protocol. - `handlerFunc` - function called when we receive a dial on `protocol. Signature must be `function (protocol, conn) {}` - `matchFunc` - matchFunc for multistream-select -### `switch.unhandle(protocol)` - -Unhandle a protocol. - -- `protocol` - -### `switch.on('peer-mux-established', (peer) => {})` - -- `peer`: is instance of [PeerInfo][] that has info of the peer we have just established a muxed connection with. - -### `switch.on('peer-mux-closed', (peer) => {})` - -- `peer`: is instance of [PeerInfo][] that has info of the peer we have just closed a muxed connection. - -### `switch.start(callback)` - -Start listening on all added transports that are available on the current `peerInfo`. - -### `switch.stop(callback)` +### `switch.hangUp(peer, callback)` -Close all the listeners and muxers. +Hang up the muxed connection we have with the peer. +- `peer`: can be an instance of [PeerInfo][], [PeerId][] or [multiaddr][] - `callback` -### `switch.connection` +### `switch.on('error', (err) => {})` -##### `switch.connection.addUpgrade()` +Emitted when the switch encounters an error. -A connection upgrade must be able to receive and return something that implements the [interface-connection](https://github.com/libp2p/interface-connection) specification. +- `err`: instance of [Error][] -> **WIP** +### `switch.on('peer-mux-established', (peer) => {})` -##### `switch.connection.addStreamMuxer(muxer)` +- `peer`: is instance of [PeerInfo][] that has info of the peer we have just established a muxed connection with. -Upgrading a connection to use a stream muxer is still considered an upgrade, but a special case since once this connection is applied, the returned obj will implement the [interface-stream-muxer](https://github.com/libp2p/interface-stream-muxer) spec. +### `switch.on('peer-mux-closed', (peer) => {})` -- `muxer` +- `peer`: is instance of [PeerInfo][] that has info of the peer we have just closed a muxed connection. -##### `switch.connection.reuse()` +### `switch.on('start', () => {})` -Enable the identify protocol. +Emitted when the switch has successfully started. -##### `switch.connection.crypto([tag, encrypt])` +### `switch.on('stop', () => {})` -Enable a specified crypto protocol. By default no encryption is used, aka `plaintext`. If called with no arguments it resets to use `plaintext`. +Emitted when the switch has successfully stopped. -You can use for example [libp2p-secio](https://github.com/libp2p/js-libp2p-secio) like this +### `switch.start(callback)` -```js -const secio = require('libp2p-secio') -switch.connection.crypto(secio.tag, secio.encrypt) -``` +Start listening on all added transports that are available on the current `peerInfo`. -##### `switch.connection.enableCircuitRelay(options, callback)` +### `switch.stop(callback)` -Enable circuit relaying. +Close all the listeners and muxers. -- `options` - - enabled - activates relay dialing and listening functionality - - hop - an object with two properties - - enabled - enables circuit relaying - - active - is it an active or passive relay (default false) - `callback` ### Stats API @@ -298,6 +305,11 @@ Each one of these values is [an exponential moving-average instance](https://git Stats are not updated in real-time. Instead, measurements are buffered and stats are updated at an interval. The maximum interval can be defined through the `Switch` constructor option `stats.computeThrottleTimeout`, defined in miliseconds. +### `switch.unhandle(protocol)` + +Unhandle a protocol. + +- `protocol` ### Internal Transports API diff --git a/src/index.js b/src/index.js index e9f934a..d0744bf 100644 --- a/src/index.js +++ b/src/index.js @@ -20,9 +20,9 @@ const log = debug('libp2p:switch') log.error = debug('libp2p:switch:error') /** - * @fires Switch#stopped Triggered when the switch has stopped - * @fires Switch#started Triggered when the switch has started - * @fires Switch#error Triggered whenever an error occurs + * @fires Switch#stop Triggered when the switch has stopped + * @fires Switch#start Triggered when the switch has started + * @fires Switch#error Triggered whenever an error occurs */ class Switch extends EventEmitter { constructor (peerInfo, peerBook, options) { @@ -201,7 +201,6 @@ class Switch extends EventEmitter { * Issues a start on the Switch state. * * @param {function} callback deprecated: Listening for the `error` and `start` events are recommended - * @fires Switch#started * @returns {void} */ start (callback = () => {}) { @@ -215,7 +214,6 @@ class Switch extends EventEmitter { * Issues a stop on the Switch state. * * @param {function} callback deprecated: Listening for the `error` and `stop` events are recommended - * @fires Switch#stop * @returns {void} */ stop (callback = () => {}) { @@ -229,7 +227,6 @@ class Switch extends EventEmitter { * A listener that will start any necessary services and listeners * * @private - * @fires Switch#error * @returns {void} */ _onStarting () { @@ -249,7 +246,6 @@ class Switch extends EventEmitter { * A listener that will turn off all running services and listeners * * @private - * @fires Switch#error * @returns {void} */ _onStopping () {