├── .eslintrc ├── protocol.proto ├── LICENSE ├── .gitignore ├── proxystream.js ├── demo └── index.js ├── package.json ├── index.js ├── README.md ├── client.js ├── messages.js └── server.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard" 3 | } -------------------------------------------------------------------------------- /protocol.proto: -------------------------------------------------------------------------------- 1 | enum EventType { 2 | CONNECT = 1; 3 | JOIN = 2; 4 | LEAVE = 3; 5 | OPEN = 4; 6 | CLOSE = 5; 7 | DATA = 6; 8 | } 9 | 10 | message SwarmEvent { 11 | required EventType type = 1; 12 | // Used for discovery key or stream id 13 | optional bytes id = 2; 14 | // Used for stream data, or for the channel when a new stream is opened 15 | optional bytes data = 3; 16 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # Screw lock files 64 | package-lock.json -------------------------------------------------------------------------------- /proxystream.js: -------------------------------------------------------------------------------- 1 | var Duplex = require('readable-stream').Duplex 2 | 3 | module.exports = class ProxyStream extends Duplex { 4 | constructor (protocol, id) { 5 | super() 6 | this._id = id 7 | this._protocol = protocol 8 | this._isClosed = false 9 | this._handle_data = this._handleData.bind(this) 10 | this._handle_close = this._handleClose.bind(this) 11 | 12 | this._protocol.on('swarm:data', this._handle_data) 13 | this._protocol.on('swarm:close', this._handle_close) 14 | } 15 | _handleData (streamid, data) { 16 | // See if the event was for this stream 17 | if (this._isId(streamid)) { 18 | this.push(data) 19 | } 20 | } 21 | _handleClose (streamid) { 22 | if (this._isId(streamid)) { 23 | this.end() 24 | this.emit('close') 25 | this._cleanup() 26 | } 27 | } 28 | _cleanup () { 29 | this._isClosed = true 30 | this._protocol.removeListener('swarm:data', this._handle_data) 31 | this._protocol.removeListener('swarm:close', this._handle_close) 32 | } 33 | _isId (streamid) { 34 | return streamid.toString('hex') === this._id.toString('hex') 35 | } 36 | _read () { } 37 | _write (chunk, encoding, callback) { 38 | this._protocol.streamData(this._id, chunk) 39 | callback() 40 | } 41 | _final (callback) { 42 | if (!this._isClosed) { 43 | this._protocol.closeStream(this._id) 44 | this._cleanup() 45 | } 46 | callback() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | var net = require('net') 2 | var hyperdrive = require('hyperdrive') 3 | var RAM = require('random-access-memory') 4 | 5 | var DSSServer = require('../server') 6 | var DSSClient = require('../client') 7 | 8 | var server = new DSSServer({}) 9 | 10 | var tcpServer = net.createServer((socket) => { 11 | console.log('Server got connection') 12 | server.addClient(socket) 13 | }) 14 | 15 | tcpServer.listen(6669, () => { 16 | // Dat website key 17 | var archiveKey = '60c525b5589a5099aa3610a8ee550dcd454c3e118f7ac93b7d41b6b850272330' 18 | 19 | addClient('127.0.0.1', 6669, archiveKey) 20 | }) 21 | 22 | function addClient (hostname, port, archiveKey) { 23 | var socket = net.connect(port, hostname) 24 | 25 | var archive = hyperdrive(RAM, archiveKey) 26 | 27 | setTimeout(() => { 28 | console.log('Reading data from archive') 29 | archive.readFile('/dat.json', 'utf-8', (err, data) => { 30 | if (err) throw err 31 | console.log('Got data:', data) 32 | }) 33 | }, 4000) 34 | 35 | var client = new DSSClient({ 36 | connection: socket, 37 | stream: (info) => { 38 | console.log('Client got a peer', info.host) 39 | var replicationStream = archive.replicate({ 40 | sparse: true, 41 | live: true 42 | }) 43 | 44 | replicationStream.on('error', (e) => { 45 | // Ignore replication errors for now 46 | }) 47 | 48 | return replicationStream 49 | } 50 | }) 51 | 52 | setTimeout(() => { 53 | client.join(archive.discoveryKey) 54 | }, 2000) 55 | } 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "discovery-swarm-stream", 3 | "version": "2.1.1", 4 | "description": "Proxy discovery over a stream", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js", 9 | "compile-protocol": "protocol-buffers protocol.proto -o messages.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/RangerMauve/discovery-swarm-stream.git" 14 | }, 15 | "keywords": [ 16 | "dat", 17 | "swarm", 18 | "dns", 19 | "discovery", 20 | "beaker" 21 | ], 22 | "author": "rangermauve", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/RangerMauve/discovery-swarm-stream/issues" 26 | }, 27 | "homepage": "https://github.com/RangerMauve/discovery-swarm-stream#readme", 28 | "devDependencies": { 29 | "eslint": "^5.0.1", 30 | "eslint-config-standard": "^11.0.0", 31 | "eslint-plugin-import": "^2.13.0", 32 | "eslint-plugin-node": "^6.0.1", 33 | "eslint-plugin-promise": "^3.8.0", 34 | "eslint-plugin-standard": "^3.1.0", 35 | "hyperdrive": "^9.14.0", 36 | "protocol-buffers": "^4.0.4", 37 | "random-access-memory": "^3.0.0", 38 | "websocket-stream": "^5.1.2" 39 | }, 40 | "dependencies": { 41 | "debug": "^4.1.1", 42 | "hyperdiscovery": "^9.0.0", 43 | "length-prefixed-message": "^3.0.3", 44 | "length-prefixed-stream": "^1.6.0", 45 | "protocol-buffers-encodings": "^1.1.0", 46 | "pump": "^3.0.0", 47 | "readable-stream": "^2.3.6", 48 | "sodium-universal": "^2.0.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Duplex = require('readable-stream').Duplex 2 | var lps = require('length-prefixed-stream') 3 | var messages = require('./messages') 4 | 5 | /* 6 | Events: 7 | 8 | `swarm:connect` Emitted when client connects to server, used to verify it's sane 9 | `swarm:join` `(key)` Emitted by the client to get the server to join a swarm for the given key 10 | `swarm:leave` `(key)` Emitted by the client to get the server to leave a swarm for the given key 11 | `swarm:open` `(id, key)` Emitted by the server when a new peer is connected for a given key. The `id` is unique per peer, and `key` is the channel key 12 | `swarm:close` `(id)` Emitted by the server when a peer's connection has closed. 13 | `swarm:data` `(id, data)` Emtited by the server or the client when data is being sent down a stream 14 | */ 15 | 16 | module.exports = class DiscoverySwarmStream extends Duplex { 17 | constructor (stream) { 18 | super() 19 | 20 | // There's going to be a lot of listeners 21 | this.setMaxListeners(256) 22 | 23 | stream 24 | .pipe(lps.decode()) 25 | .pipe(this) 26 | .pipe(lps.encode()) 27 | .pipe(stream) 28 | } 29 | 30 | sendEvent (type, id, data) { 31 | this.push(messages.SwarmEvent.encode({ 32 | type: messages.EventType[type], 33 | id: id, 34 | data: data 35 | })) 36 | } 37 | 38 | connect () { 39 | this.sendEvent('CONNECT') 40 | } 41 | 42 | join (discoveryKey) { 43 | this.sendEvent('JOIN', discoveryKey) 44 | } 45 | 46 | leave (discoveryKey) { 47 | this.sendEvent('LEAVE', discoveryKey) 48 | } 49 | 50 | openStream (streamId, channel) { 51 | this.sendEvent('OPEN', streamId, channel) 52 | } 53 | 54 | closeStream (streamId) { 55 | this.sendEvent('CLOSE', streamId) 56 | } 57 | 58 | streamData (streamId, data) { 59 | this.sendEvent('DATA', streamId, data) 60 | } 61 | 62 | _write (chunk, encoding, callback) { 63 | try { 64 | var decoded = messages.SwarmEvent.decode(chunk) 65 | switch (decoded.type) { 66 | case (messages.EventType.CONNECT): this.emit('swarm:connect'); break 67 | case (messages.EventType.JOIN): this.emit('swarm:join', decoded.id); break 68 | case (messages.EventType.LEAVE): this.emit('swarm:leave', decoded.id); break 69 | case (messages.EventType.OPEN): this.emit('swarm:open', decoded.id, decoded.data); break 70 | case (messages.EventType.CLOSE): this.emit('swarm:close', decoded.id); break 71 | case (messages.EventType.DATA) : this.emit('swarm:data', decoded.id, decoded.data); break 72 | } 73 | callback() 74 | } catch (e) { 75 | callback(e) 76 | } 77 | } 78 | 79 | _read () {} 80 | } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discovery Swarm Stream 2 | 3 | Alows clients to use [discovery-channel](https://github.com/maxogden/discovery-channel) to discover and connect to peers. 4 | 5 | Clients connect to the server, search for "discovery keys", and the proxy automatically discovers and connects to peers and then proxies those connections to the client. 6 | 7 | If two clients are discovering the same key, the proxy can connect them to each other if you set the `connectExistingClients` on the server. 8 | 9 | By default it supports connections for the [hypercore-protocol](https://github.com/mafintosh/hypercore-protocol) used in [Dat](https://datproject.org/). If you'd like to support a different protocol, provide a `stream` argument that conforms to what's expected in [discovery-swarm](https://www.npmjs.com/package/discovery-swarm#var-sw--swarmopts). 10 | 11 | Requires: 12 | 13 | - ES6 classes 14 | - Arrow functions 15 | - Weak Sets (server only) 16 | 17 | ## Example 18 | 19 | ```javascript 20 | // On a server 21 | const DSS = require('discovery-swarm-stream/server') 22 | 23 | const swarm = new DSS({ 24 | // Hash topics through sha1 before passing them through 25 | defaultHash: false, 26 | 27 | // Use the default discovery-swarm handshaking 28 | defaultHandshake: false, 29 | 30 | // swarm options here 31 | // Doesn't support UTP for now 32 | }) 33 | 34 | const httpServer = require('http').createServer() 35 | httpServer.listen(4200) 36 | 37 | const server = require('websocket-stream').createServer({server: httpServer}, (ws) => { 38 | swarm.handleConnection(ws) 39 | }) 40 | 41 | // On a client 42 | const DSS = require('discovery-swarm-stream/client') 43 | const websocket = require('websocket-stream') 44 | 45 | const socket = websocket('ws://localhost:4200') 46 | 47 | const swarm = new DSS({ 48 | connection: socket, 49 | stream: (connection) => connection.write('hello!') 50 | }) 51 | 52 | swarm.join('wowcool') 53 | 54 | swarm.leave('wowcool') 55 | 56 | // If you want to add auto-reconnect logic 57 | swarm.on('disconnected', () => { 58 | swarm.reconnect(websocket('ws://localhost:4200')) 59 | }) 60 | 61 | setTimeout(() => { 62 | swarm.close() 63 | }, 10000) 64 | ``` 65 | 66 | Check out `demo/index.js` for an example of how this can be used with hyperdrive. 67 | 68 | ### Protocol 69 | 70 | Sent to the server 71 | - connect 72 | - join(discoveryKey) 73 | - leave(discoveryKey) 74 | 75 | Sent from the server 76 | - streamOpen(streamID) 77 | 78 | Sent from either end 79 | - streamData(streamId, data) 80 | - streamClose(streamId) 81 | 82 | ### Behind the scenes 83 | - Take a discovery-swarm instance 84 | - When getting a new connection 85 | - Find out how many clients want it's advertising keys 86 | - If only one exists, proxy the connection to it 87 | - I multiples exist, create new connections per client and proxy them 88 | - Client side will get streams and should do handshaking on them themselves 89 | -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events') 2 | 3 | var DiscoverySwarmStream = require('./') 4 | var ProxyStream = require('./proxystream') 5 | 6 | /* 7 | Create map of [streamid] => [node stream] 8 | Emit handshaking event when getting the `open` event 9 | Emit the connection and connection-closed events 10 | Peer info objects look similar, but use `-1` for the port and use the ID to hex as the host. Initiator is always false. Type is `proxy` 11 | */ 12 | 13 | module.exports = class DiscoverySwarmClient extends EventEmitter { 14 | constructor (options) { 15 | super() 16 | var connection = options.connection 17 | if (!connection) throw new TypeError('Must specify `connection` in options') 18 | this.connecting = 0 19 | this.queued = 0 20 | this.connected = 0 21 | 22 | this._handleOpen = this._handleOpen.bind(this) 23 | this._handleEnd = this._handleEnd.bind(this) 24 | 25 | if (options.stream) { 26 | this._replicate = options.stream 27 | } 28 | 29 | this._channels = new Set() 30 | 31 | this.reconnect(connection) 32 | } 33 | 34 | reconnect (connection) { 35 | if (this._protocol) { 36 | this._protocol.removeListener('close', this._handleEnd) 37 | this._protocol.end() 38 | } 39 | this._protocol = new DiscoverySwarmStream(connection) 40 | this._protocol.on('swarm:open', this._handleOpen) 41 | this._protocol.connect() 42 | this._protocol.once('close', this._handleEnd) 43 | 44 | for (let key of this._channels) { 45 | this.join(key) 46 | } 47 | } 48 | 49 | _handleEnd () { 50 | this._protocol = null 51 | this.emit('disconnected') 52 | } 53 | 54 | _handleOpen (streamid, channel) { 55 | var stream = new ProxyStream(this._protocol, streamid) 56 | // Save locally 57 | var info = { 58 | type: 'proxy', 59 | initiator: false, 60 | id: null, 61 | host: streamid.toString('hex'), 62 | port: -1, 63 | channel: channel 64 | } 65 | 66 | var replicationStream = this._replicate(info) 67 | var self = this 68 | 69 | self.emit('handshaking', stream, info) 70 | 71 | replicationStream.once('handshake', function (remoteId) { 72 | if (remoteId) { 73 | var remoteIdHex = remoteId.toString('hex') 74 | info.id = remoteIdHex 75 | } 76 | self.emit('connection', stream, info) 77 | }) 78 | 79 | replicationStream.pipe(stream).pipe(replicationStream) 80 | } 81 | 82 | join (key, options, cb) { 83 | if (typeof key === 'string') { 84 | key = Buffer.from(key, 'hex') 85 | } 86 | if (!cb && (typeof options === 'function')) { 87 | cb = options 88 | } 89 | this._protocol.join(key) 90 | 91 | this._channels.add(key.toString('hex')) 92 | if (cb) cb() 93 | } 94 | 95 | leave (key, cb) { 96 | if (typeof key === 'string') { 97 | key = Buffer.from(key, 'hex') 98 | } 99 | 100 | this._protocol.leave(key) 101 | 102 | this._channels.delete(key.toString('hex')) 103 | 104 | if (cb) cb() 105 | } 106 | 107 | listen () { 108 | // No-op, just in case 109 | } 110 | 111 | close (cb) { 112 | this._protocol.end(cb) 113 | } 114 | 115 | _replicate (info) { 116 | // TODO: Do the default handshake thing for replication 117 | throw new Error('Missing `stream` in options') 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /messages.js: -------------------------------------------------------------------------------- 1 | // This file is auto generated by the protocol-buffers cli tool 2 | 3 | /* eslint-disable quotes */ 4 | /* eslint-disable indent */ 5 | /* eslint-disable no-redeclare */ 6 | /* eslint-disable camelcase */ 7 | 8 | // Remember to `npm install --save protocol-buffers-encodings` 9 | var encodings = require('protocol-buffers-encodings') 10 | var varint = encodings.varint 11 | var skip = encodings.skip 12 | 13 | exports.EventType = { 14 | "CONNECT": 1, 15 | "JOIN": 2, 16 | "LEAVE": 3, 17 | "OPEN": 4, 18 | "CLOSE": 5, 19 | "DATA": 6 20 | } 21 | 22 | var SwarmEvent = exports.SwarmEvent = { 23 | buffer: true, 24 | encodingLength: null, 25 | encode: null, 26 | decode: null 27 | } 28 | 29 | defineSwarmEvent() 30 | 31 | function defineSwarmEvent () { 32 | var enc = [ 33 | encodings.enum, 34 | encodings.bytes 35 | ] 36 | 37 | SwarmEvent.encodingLength = encodingLength 38 | SwarmEvent.encode = encode 39 | SwarmEvent.decode = decode 40 | 41 | function encodingLength (obj) { 42 | var length = 0 43 | if (!defined(obj.type)) throw new Error("type is required") 44 | var len = enc[0].encodingLength(obj.type) 45 | length += 1 + len 46 | if (defined(obj.id)) { 47 | var len = enc[1].encodingLength(obj.id) 48 | length += 1 + len 49 | } 50 | if (defined(obj.data)) { 51 | var len = enc[1].encodingLength(obj.data) 52 | length += 1 + len 53 | } 54 | return length 55 | } 56 | 57 | function encode (obj, buf, offset) { 58 | if (!offset) offset = 0 59 | if (!buf) buf = Buffer.allocUnsafe(encodingLength(obj)) 60 | var oldOffset = offset 61 | if (!defined(obj.type)) throw new Error("type is required") 62 | buf[offset++] = 8 63 | enc[0].encode(obj.type, buf, offset) 64 | offset += enc[0].encode.bytes 65 | if (defined(obj.id)) { 66 | buf[offset++] = 18 67 | enc[1].encode(obj.id, buf, offset) 68 | offset += enc[1].encode.bytes 69 | } 70 | if (defined(obj.data)) { 71 | buf[offset++] = 26 72 | enc[1].encode(obj.data, buf, offset) 73 | offset += enc[1].encode.bytes 74 | } 75 | encode.bytes = offset - oldOffset 76 | return buf 77 | } 78 | 79 | function decode (buf, offset, end) { 80 | if (!offset) offset = 0 81 | if (!end) end = buf.length 82 | if (!(end <= buf.length && offset <= buf.length)) throw new Error("Decoded message is not valid") 83 | var oldOffset = offset 84 | var obj = { 85 | type: 1, 86 | id: null, 87 | data: null 88 | } 89 | var found0 = false 90 | while (true) { 91 | if (end <= offset) { 92 | if (!found0) throw new Error("Decoded message is not valid") 93 | decode.bytes = offset - oldOffset 94 | return obj 95 | } 96 | var prefix = varint.decode(buf, offset) 97 | offset += varint.decode.bytes 98 | var tag = prefix >> 3 99 | switch (tag) { 100 | case 1: 101 | obj.type = enc[0].decode(buf, offset) 102 | offset += enc[0].decode.bytes 103 | found0 = true 104 | break 105 | case 2: 106 | obj.id = enc[1].decode(buf, offset) 107 | offset += enc[1].decode.bytes 108 | break 109 | case 3: 110 | obj.data = enc[1].decode(buf, offset) 111 | offset += enc[1].decode.bytes 112 | break 113 | default: 114 | offset = skip(prefix & 7, buf, offset) 115 | } 116 | } 117 | } 118 | } 119 | 120 | function defined (val) { 121 | return val !== null && val !== undefined && (typeof val !== 'number' || !isNaN(val)) 122 | } 123 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const sodium = require('sodium-universal') 2 | const EventEmitter = require('events') 3 | const net = require('net') 4 | const createDiscovery = require('hyperdiscovery') 5 | const crypto = require('crypto') 6 | const pump = require('pump') 7 | const lpmessage = require('length-prefixed-message') 8 | const DiscoverySwarmStream = require('./') 9 | const ProxyStream = require('./proxystream') 10 | const debug = require('debug')('discovery-swarm-stream:server') 11 | 12 | module.exports = class DiscoverySwarmStreamServer extends EventEmitter { 13 | constructor (options) { 14 | super() 15 | if (!options) { 16 | options = {} 17 | } 18 | this.options = options 19 | 20 | this.connectExistingClients = !!options.connectExistingClients 21 | this._discovery = createDiscovery(options) 22 | 23 | // For making sure other peers don't remember us 24 | // Otherwise they'll block new connections if we connected before 25 | this._discovery.id = null 26 | 27 | // I am not proud of this code, but it works! :D 28 | const createStream = options.stream || this._discovery._createReplicationStream.bind(this._discovery) 29 | const stream = (info) => { 30 | const stream = createStream(info) 31 | 32 | debug('got connection', info) 33 | 34 | // This needs to be done so that we can connect to this peer agian 35 | const shortId = info.host + ':' + info.port 36 | 37 | const peersSeen = this._discovery._swarm._peersSeen 38 | if (peersSeen[shortId]) peersSeen[shortId] = 0 39 | 40 | const emitKeyAndClose = (key) => { 41 | debug('got key from connection', key, info) 42 | this.emit('key:' + key.toString('hex'), key, info) 43 | stream.end() 44 | 45 | // This needs to be done so that we can connect to this peer agian 46 | const longId = shortId + '@' + key.toString('hex') 47 | if (peersSeen[longId]) peersSeen[longId] = 0 48 | } 49 | 50 | if (info.channel) { 51 | process.nextTick(() => { 52 | emitKeyAndClose(info.channel) 53 | }) 54 | } 55 | 56 | stream.on('feed', emitKeyAndClose) 57 | 58 | return stream 59 | } 60 | 61 | // Use this option to hash topics with sha1 62 | if(options.defaultHash) { 63 | delete this._discovery._swarm._options.hash 64 | } 65 | 66 | // Use this option to enable the default discovery-swarm handshake 67 | if(options.defaultHandshake) { 68 | this._discovery._swarm._stream = null 69 | this._discovery._swarm.on('connection', (connection, info) => { 70 | const replicationStream = stream(info) 71 | pump(connection, replicationStream, connection) 72 | }) 73 | } else { 74 | this._discovery._swarm._stream = stream 75 | } 76 | 77 | 78 | // We don't need any connections after we have their discovery key 79 | this._discovery.on('connection', (connection) => { 80 | if(connection.close) { 81 | connection.close() 82 | } else if(connection.destroy) { 83 | connection.destroy() 84 | } else if(connection.end) { 85 | connection.end() 86 | } 87 | }) 88 | 89 | this._discovery.on('close', () => this.emit('close')) 90 | 91 | // List of clients 92 | this._clients = [] 93 | // Map of weaksets that looks like `subscription -> [clients]` 94 | this._subs = {} 95 | } 96 | 97 | _joinClient (key, client) { 98 | if (!this._subs[key]) { 99 | this._subs[key] = [] 100 | } 101 | 102 | var subs = this._subs[key] 103 | 104 | subs.push(client) 105 | 106 | this.join(key) 107 | } 108 | 109 | _leaveClient (key, client) { 110 | var subs = this._subs[key] 111 | 112 | if (!subs) return 113 | 114 | var index = subs.indexOf(client) 115 | if (index === -1) return 116 | 117 | subs.splice(index, 1) 118 | 119 | if (!subs.length) { 120 | this.leave(key) 121 | } 122 | } 123 | 124 | join (key) { 125 | this._discovery._swarm.leave(key) 126 | this._discovery._swarm.join(key) 127 | } 128 | 129 | leave (key) { 130 | this._discovery._swarm.leave(key) 131 | } 132 | 133 | subscribedClients (key) { 134 | var subs = this._subs[key] 135 | return subs || [] 136 | } 137 | 138 | destroy (cb) { 139 | this._discovery.destroy(cb) 140 | this._clients.forEach((client) => { 141 | client.destroy() 142 | }) 143 | } 144 | 145 | addClient (stream) { 146 | var client = new Client(stream) 147 | this._clients.push(client) 148 | 149 | debug('incoming client', client.id.toString('hex')) 150 | 151 | // TODO: Add timeout and clear on "connection" packet 152 | client.once('swarm:connect', client.init.bind(client, this)) 153 | client.once('close', () => { 154 | this._clients.splice(this._clients.indexOf(client), 1) 155 | }) 156 | 157 | return client 158 | } 159 | } 160 | 161 | class Client extends DiscoverySwarmStream { 162 | constructor (stream) { 163 | super(stream) 164 | this._connections = {} 165 | this._subscriptions = [] 166 | this.connectTCP = this.connectTCP.bind(this) 167 | this.destroy = this.destroy.bind(this) 168 | 169 | var id = Buffer.allocUnsafe(12) // Cryptographically random data 170 | sodium.randombytes_buf(id) 171 | this.id = id 172 | 173 | stream.once('close', this.destroy.bind(this)) 174 | } 175 | 176 | init (swarm) { 177 | this._swarm = swarm 178 | 179 | this.on('swarm:join', (key) => { 180 | debug('joining discovery key', this.id.toString('hex'), key) 181 | var stringKey = key.toString('hex') 182 | this._swarm.on('key:' + stringKey, this.connectTCP) 183 | this._subscriptions.push(stringKey) 184 | this._swarm._joinClient(key, this) 185 | 186 | // Don't connect clients together unless you need it 187 | // This is to encourage connections through WebRTC 188 | if (!this._swarm.connectExistingClients) return 189 | 190 | var existing = this._swarm.subscribedClients(key) 191 | 192 | existing.forEach((client) => { 193 | // Don't connect to yourself 194 | if (client === this) return 195 | this.connectClient(key, client) 196 | }) 197 | }) 198 | 199 | this.on('swarm:leave', (key) => { 200 | var stringKey = key.toString('hex') 201 | this._swarm.removeListener('key:' + stringKey, this.connectTCP) 202 | this._subscriptions = this._subscriptions.filter((existing) => { 203 | return existing !== stringKey 204 | }) 205 | this._swarm._leaveClient(key, this) 206 | }) 207 | } 208 | 209 | destroy () { 210 | Object.keys(this._connections).forEach((id) => { 211 | var connection = this._connections[id] 212 | if (connection) { 213 | connection.end() 214 | } 215 | }) 216 | this._subscriptions.forEach((stringKey) => { 217 | const key = Buffer.from(stringKey, 'hex') 218 | this._swarm.removeListener('key:' + stringKey, this.connectTCP) 219 | this._swarm._leaveClient(key, this) 220 | }) 221 | } 222 | 223 | connectStream (key, stream, peerId) { 224 | var id = Buffer.allocUnsafe(12) // Cryptographically random data 225 | sodium.randombytes_buf(id) 226 | 227 | this.openStream(id, key) 228 | 229 | var proxy = new ProxyStream(this, id) 230 | 231 | proxy.on('close', () => stream.end()) 232 | 233 | stream.once('close', () => { 234 | this._connections[peerId] = null 235 | }) 236 | 237 | stream.once('error', () => { 238 | proxy.end() 239 | this._connections[peerId] = null 240 | }) 241 | 242 | stream.pipe(proxy).pipe(stream) 243 | } 244 | 245 | connectClient (key, client) { 246 | var id = key + ':' + client.id 247 | 248 | if (this._connections[id]) { 249 | return this._connections[id] 250 | } 251 | 252 | var otherProxy = new ProxyStream(client, key) 253 | 254 | this.connectStream(key, otherProxy, id) 255 | } 256 | 257 | connectTCP (key, peer) { 258 | var id = peer.id || (key + ':' + peer.host + ':' + peer.port) 259 | if (this._connections[id]) { 260 | return this._connections[id] 261 | } 262 | 263 | debug('making outgoing connection', this.id.toString('hex'), key, peer) 264 | 265 | var connection = net.connect(peer.port, peer.host) 266 | this._connections[id] = connection 267 | 268 | if(this._swarm.options.defaultHandshake) { 269 | lpmessage.write(connection, this.id) 270 | lpmessage.read(connection, (err, id) => { 271 | this.connectStream(key, connection, id) 272 | }) 273 | } else { 274 | this.connectStream(key, connection, id) 275 | } 276 | } 277 | 278 | toString () { 279 | return this.id.toString() 280 | } 281 | } 282 | 283 | function sha1 (id) { 284 | return crypto.createHash('sha1').update(id).digest() 285 | } 286 | --------------------------------------------------------------------------------