├── .gitignore ├── LICENSE ├── README.md ├── cast.js ├── example.js ├── index.js ├── messages.js ├── package.json └── schema.proto /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Mathias Buus 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hypercore-multicast-swarm 2 | 3 | Multicast [hypercore](https://github.com/mafintosh/hypercore) data over a multicast UDP socket 4 | 5 | ``` 6 | npm install hypercore-multicast-swarm 7 | ``` 8 | 9 | Still does all the data verification that hypercore normally does etc, it's just multicast! 10 | 11 | ## Usage 12 | 13 | ``` js 14 | const mswarm = require('hypercore-multicast-swarm') 15 | 16 | // this will make the hypercore listen for multicast data on the network. 17 | // when it receives a message it doesn't have it will verify it and store it. 18 | const swarm = mswarm(someHypercoreFeed) 19 | 20 | someHypercoreFeed.on('download', function (seq, data) { 21 | console.log('we recevied ' + seq + ' over multicast') 22 | }) 23 | 24 | // to multicast a hypercore entry 25 | swarm.multicast(42) // multicasts entry 42 26 | ``` 27 | 28 | ## API 29 | 30 | #### `swarm = mswarm(feed, [options])` 31 | 32 | Make a hypercore feed join the multicast swarm. Options include: 33 | 34 | ```js 35 | { 36 | mtu: 900, 37 | port: 5007, 38 | address: '224.1.1.1' // the multicast address to use 39 | } 40 | ``` 41 | 42 | No hypercore messages are multicast until you call the `multicast` api below 43 | 44 | #### `swarm.multicast(seq, [callback])` 45 | 46 | Multicast the entry stored at `seq` in the feed. 47 | The callback is called when the underlying udp socket has been flushed. 48 | 49 | ## License 50 | 51 | MIT 52 | -------------------------------------------------------------------------------- /cast.js: -------------------------------------------------------------------------------- 1 | const cyclist = require('cyclist') 2 | const dgram = require('dgram') 3 | const util = require('util') 4 | const sodium = require('sodium-universal') 5 | const events = require('events') 6 | 7 | module.exports = Cast 8 | 9 | function Cast (key, opts) { 10 | if (!(this instanceof Cast)) return new Cast(key, opts) 11 | events.EventEmitter.call(this) 12 | 13 | if (!opts) opts = {} 14 | 15 | const self = this 16 | 17 | this.key = key 18 | this.seq = 0 19 | this.mtu = opts.mtu || 900 20 | this.incoming = cyclist(16384) 21 | this.address = opts.address || '224.1.1.1' 22 | this.port = opts.port || 5007 23 | this.socket = dgram.createSocket({ 24 | type: 'udp4', 25 | reuseAddr: true 26 | }) 27 | 28 | this.socket.on('message', this.onmessage.bind(this)) 29 | 30 | this.socket.on('close', function () { 31 | self.emit('close') 32 | }) 33 | 34 | this.socket.on('error', function (err) { 35 | self.emit('warning', err) 36 | }) 37 | 38 | this.socket.bind(this.port, function () { 39 | self.socket.addMembership(self.address) 40 | self.socket.setMulticastTTL(128) 41 | self.emit('bind') 42 | }) 43 | } 44 | 45 | util.inherits(Cast, events.EventEmitter) 46 | 47 | Cast.prototype.close = function () { 48 | this.socket.close() 49 | } 50 | 51 | Cast.prototype.onmessage = function (buf) { 52 | if (buf.length <= 40) return 53 | 54 | const nonce = buf.slice(0, 24) 55 | const message = buf.slice(24, buf.length - 16) 56 | const mac = buf.slice(buf.length - 16) 57 | 58 | if (!sodium.crypto_secretbox_open_detached(message, message, mac, nonce, this.key)) return 59 | 60 | buf = message 61 | 62 | const header = buf.readUInt16BE(0) 63 | const seq = header >> 2 64 | 65 | this.incoming.put(seq, buf) 66 | 67 | var left = seq 68 | var right = seq 69 | 70 | while (needsPrev(buf)) { 71 | buf = this.incoming.get(--left) 72 | if (!buf) return false 73 | } 74 | 75 | buf = this.incoming.get(seq) 76 | 77 | while (needsNext(buf)) { 78 | buf = this.incoming.get(++right) 79 | if (!buf) return false 80 | } 81 | 82 | const buffers = [] 83 | for (var i = left; i <= right; i++) { 84 | buffers.push(this.incoming.get(i).slice(2)) 85 | } 86 | if (buffers.length === 1) this.onfullmessage(buffers[0]) 87 | else this.onfullmessage(Buffer.concat(buffers)) 88 | } 89 | 90 | Cast.prototype.onfullmessage = function (buf) { 91 | this.emit('message', buf) 92 | } 93 | 94 | function needsPrev (buf) { 95 | return buf[1] & 1 96 | } 97 | 98 | function needsNext (buf) { 99 | return buf[1] & 2 100 | } 101 | 102 | Cast.prototype.multicast = function (buf, cb) { 103 | const buffers = this.encode(buf) 104 | var error = null 105 | var missing = buffers.length 106 | 107 | for (var i = 0; i < buffers.length; i++) { 108 | this.socket.send(buffers[i], 0, buffers[i].length, this.port, this.address, done) 109 | } 110 | 111 | function done (err) { 112 | if (err) error = err 113 | if (!--missing && cb) cb(error) 114 | } 115 | } 116 | 117 | Cast.prototype.encode = function (buf) { 118 | const key = this.key 119 | const buffers = [] 120 | const mtu = Math.max(1, this.mtu - 2 - 24 - 16) 121 | var i = 0 122 | 123 | for (i = 0; i < buf.length; i += mtu) { 124 | buffers.push(buf.slice(i, i + mtu)) 125 | } 126 | 127 | for (i = 0; i < buffers.length; i++) { 128 | const wrap = Buffer.allocUnsafe(buffers[i].length + 2 + 24 + 16) 129 | const mac = wrap.slice(wrap.length - 16) 130 | const nonce = wrap.slice(0, 24) 131 | const message = wrap.slice(24, wrap.length - 16) 132 | const header = (this._tick() << 2) + (i < buffers.length - 1 ? 2 : 0) + (i > 0 ? 1 : 0) 133 | 134 | message.writeUInt16BE(header, 0) 135 | buffers[i].copy(message, 2) 136 | sodium.randombytes_buf(nonce) 137 | 138 | sodium.crypto_secretbox_detached(message, mac, message, nonce, key) 139 | buffers[i] = wrap 140 | } 141 | 142 | return buffers 143 | } 144 | 145 | Cast.prototype._tick = function () { 146 | const seq = this.seq++ 147 | if (this.seq === 16384) this.seq = 0 148 | return seq 149 | } 150 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | const swarm = require('./') 2 | const hypercore = require('hypercore') 3 | const ram = require('random-access-memory') 4 | 5 | const feed = hypercore(ram) 6 | const sw = swarm(feed) 7 | 8 | feed.append(Buffer.alloc(99999)) 9 | feed.append('hello world', function () { 10 | createSwarm(feed.key).on('bind', function () { 11 | sw.multicast(0) 12 | sw.multicast(1) 13 | }) 14 | }) 15 | 16 | function createSwarm (key) { 17 | const feed = hypercore(ram, key) 18 | feed.on('download', console.log) 19 | return swarm(feed) 20 | } 21 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const cast = require('./cast') 2 | const messages = require('./messages') 3 | const events = require('events') 4 | const util = require('util') 5 | 6 | module.exports = Swarm 7 | 8 | function Swarm (feed, opts) { 9 | if (!(this instanceof Swarm)) return new Swarm(feed, opts) 10 | events.EventEmitter.call(this) 11 | 12 | const self = this 13 | 14 | this.feed = feed 15 | this.cast = null 16 | 17 | feed.ready(function (err) { 18 | if (err) return self.emit('error', err) 19 | 20 | self.cast = cast(feed.key, opts) 21 | self.cast.on('message', self._onmessage.bind(self)) 22 | self.cast.on('bind', self.emit.bind(self, 'bind')) 23 | self.cast.on('close', self.emit.bind(self, 'close')) 24 | 25 | self.emit('ready') 26 | }) 27 | } 28 | 29 | util.inherits(Swarm, events.EventEmitter) 30 | 31 | Swarm.prototype.close = function () { 32 | if (!this.cast) this.once('ready', this.close) 33 | else this.cast.close() 34 | } 35 | 36 | Swarm.prototype._onmessage = function (buf) { 37 | const msg = decodeMessage(buf) 38 | if (!msg) return 39 | if (this.feed.bitfield && this.feed.has(msg.seq)) return 40 | 41 | const self = this 42 | 43 | // TODO: make feed.put support valueEncoding 44 | this.feed._putBuffer(msg.seq, msg.data, msg, null, function (err) { 45 | if (err) self.emit('bad-message', msg) 46 | }) 47 | } 48 | 49 | Swarm.prototype.multicast = function (seq, cb) { 50 | if (!cb) cb = noop 51 | const self = this 52 | this.feed.get(seq, {valueEncoding: 'binary'}, function (err, data) { 53 | if (err) return cb(err) 54 | self.feed.proof(seq, function (err, proof) { 55 | if (err) return cb(err) 56 | 57 | const buf = messages.Message.encode({ 58 | nodes: proof.nodes, 59 | signature: proof.signature, 60 | seq, 61 | data 62 | }) 63 | 64 | // always set here, as .get runs after feed.ready 65 | self.cast.multicast(buf, cb) 66 | }) 67 | }) 68 | } 69 | 70 | function noop () {} 71 | 72 | function decodeMessage (buf) { 73 | try { 74 | return messages.Message.decode(buf) 75 | } catch (err) { 76 | return null 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /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 | var Node = exports.Node = { 14 | buffer: true, 15 | encodingLength: null, 16 | encode: null, 17 | decode: null 18 | } 19 | 20 | var Message = exports.Message = { 21 | buffer: true, 22 | encodingLength: null, 23 | encode: null, 24 | decode: null 25 | } 26 | 27 | defineNode() 28 | defineMessage() 29 | 30 | function defineNode () { 31 | var enc = [ 32 | encodings.varint, 33 | encodings.bytes 34 | ] 35 | 36 | Node.encodingLength = encodingLength 37 | Node.encode = encode 38 | Node.decode = decode 39 | 40 | function encodingLength (obj) { 41 | var length = 0 42 | if (!defined(obj.index)) throw new Error("index is required") 43 | var len = enc[0].encodingLength(obj.index) 44 | length += 1 + len 45 | if (!defined(obj.hash)) throw new Error("hash is required") 46 | var len = enc[1].encodingLength(obj.hash) 47 | length += 1 + len 48 | if (!defined(obj.size)) throw new Error("size is required") 49 | var len = enc[0].encodingLength(obj.size) 50 | length += 1 + len 51 | return length 52 | } 53 | 54 | function encode (obj, buf, offset) { 55 | if (!offset) offset = 0 56 | if (!buf) buf = Buffer.allocUnsafe(encodingLength(obj)) 57 | var oldOffset = offset 58 | if (!defined(obj.index)) throw new Error("index is required") 59 | buf[offset++] = 8 60 | enc[0].encode(obj.index, buf, offset) 61 | offset += enc[0].encode.bytes 62 | if (!defined(obj.hash)) throw new Error("hash is required") 63 | buf[offset++] = 18 64 | enc[1].encode(obj.hash, buf, offset) 65 | offset += enc[1].encode.bytes 66 | if (!defined(obj.size)) throw new Error("size is required") 67 | buf[offset++] = 24 68 | enc[0].encode(obj.size, buf, offset) 69 | offset += enc[0].encode.bytes 70 | encode.bytes = offset - oldOffset 71 | return buf 72 | } 73 | 74 | function decode (buf, offset, end) { 75 | if (!offset) offset = 0 76 | if (!end) end = buf.length 77 | if (!(end <= buf.length && offset <= buf.length)) throw new Error("Decoded message is not valid") 78 | var oldOffset = offset 79 | var obj = { 80 | index: 0, 81 | hash: null, 82 | size: 0 83 | } 84 | var found0 = false 85 | var found1 = false 86 | var found2 = false 87 | while (true) { 88 | if (end <= offset) { 89 | if (!found0 || !found1 || !found2) throw new Error("Decoded message is not valid") 90 | decode.bytes = offset - oldOffset 91 | return obj 92 | } 93 | var prefix = varint.decode(buf, offset) 94 | offset += varint.decode.bytes 95 | var tag = prefix >> 3 96 | switch (tag) { 97 | case 1: 98 | obj.index = enc[0].decode(buf, offset) 99 | offset += enc[0].decode.bytes 100 | found0 = true 101 | break 102 | case 2: 103 | obj.hash = enc[1].decode(buf, offset) 104 | offset += enc[1].decode.bytes 105 | found1 = true 106 | break 107 | case 3: 108 | obj.size = enc[0].decode(buf, offset) 109 | offset += enc[0].decode.bytes 110 | found2 = true 111 | break 112 | default: 113 | offset = skip(prefix & 7, buf, offset) 114 | } 115 | } 116 | } 117 | } 118 | 119 | function defineMessage () { 120 | var enc = [ 121 | Node, 122 | encodings.bytes, 123 | encodings.varint 124 | ] 125 | 126 | Message.encodingLength = encodingLength 127 | Message.encode = encode 128 | Message.decode = decode 129 | 130 | function encodingLength (obj) { 131 | var length = 0 132 | if (defined(obj.nodes)) { 133 | for (var i = 0; i < obj.nodes.length; i++) { 134 | if (!defined(obj.nodes[i])) continue 135 | var len = enc[0].encodingLength(obj.nodes[i]) 136 | length += varint.encodingLength(len) 137 | length += 1 + len 138 | } 139 | } 140 | if (defined(obj.signature)) { 141 | var len = enc[1].encodingLength(obj.signature) 142 | length += 1 + len 143 | } 144 | if (!defined(obj.seq)) throw new Error("seq is required") 145 | var len = enc[2].encodingLength(obj.seq) 146 | length += 1 + len 147 | if (defined(obj.data)) { 148 | var len = enc[1].encodingLength(obj.data) 149 | length += 1 + len 150 | } 151 | return length 152 | } 153 | 154 | function encode (obj, buf, offset) { 155 | if (!offset) offset = 0 156 | if (!buf) buf = Buffer.allocUnsafe(encodingLength(obj)) 157 | var oldOffset = offset 158 | if (defined(obj.nodes)) { 159 | for (var i = 0; i < obj.nodes.length; i++) { 160 | if (!defined(obj.nodes[i])) continue 161 | buf[offset++] = 10 162 | varint.encode(enc[0].encodingLength(obj.nodes[i]), buf, offset) 163 | offset += varint.encode.bytes 164 | enc[0].encode(obj.nodes[i], buf, offset) 165 | offset += enc[0].encode.bytes 166 | } 167 | } 168 | if (defined(obj.signature)) { 169 | buf[offset++] = 18 170 | enc[1].encode(obj.signature, buf, offset) 171 | offset += enc[1].encode.bytes 172 | } 173 | if (!defined(obj.seq)) throw new Error("seq is required") 174 | buf[offset++] = 24 175 | enc[2].encode(obj.seq, buf, offset) 176 | offset += enc[2].encode.bytes 177 | if (defined(obj.data)) { 178 | buf[offset++] = 34 179 | enc[1].encode(obj.data, buf, offset) 180 | offset += enc[1].encode.bytes 181 | } 182 | encode.bytes = offset - oldOffset 183 | return buf 184 | } 185 | 186 | function decode (buf, offset, end) { 187 | if (!offset) offset = 0 188 | if (!end) end = buf.length 189 | if (!(end <= buf.length && offset <= buf.length)) throw new Error("Decoded message is not valid") 190 | var oldOffset = offset 191 | var obj = { 192 | nodes: [], 193 | signature: null, 194 | seq: 0, 195 | data: null 196 | } 197 | var found2 = false 198 | while (true) { 199 | if (end <= offset) { 200 | if (!found2) throw new Error("Decoded message is not valid") 201 | decode.bytes = offset - oldOffset 202 | return obj 203 | } 204 | var prefix = varint.decode(buf, offset) 205 | offset += varint.decode.bytes 206 | var tag = prefix >> 3 207 | switch (tag) { 208 | case 1: 209 | var len = varint.decode(buf, offset) 210 | offset += varint.decode.bytes 211 | obj.nodes.push(enc[0].decode(buf, offset, offset + len)) 212 | offset += enc[0].decode.bytes 213 | break 214 | case 2: 215 | obj.signature = enc[1].decode(buf, offset) 216 | offset += enc[1].decode.bytes 217 | break 218 | case 3: 219 | obj.seq = enc[2].decode(buf, offset) 220 | offset += enc[2].decode.bytes 221 | found2 = true 222 | break 223 | case 4: 224 | obj.data = enc[1].decode(buf, offset) 225 | offset += enc[1].decode.bytes 226 | break 227 | default: 228 | offset = skip(prefix & 7, buf, offset) 229 | } 230 | } 231 | } 232 | } 233 | 234 | function defined (val) { 235 | return val !== null && val !== undefined && (typeof val !== 'number' || !isNaN(val)) 236 | } 237 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hypercore-multicast-swarm", 3 | "version": "2.0.0", 4 | "description": "Multicast hypercore data over a multicast UDP socket", 5 | "main": "index.js", 6 | "dependencies": { 7 | "cyclist": "^1.0.1", 8 | "protocol-buffers-encodings": "^1.1.0", 9 | "sodium-universal": "^2.0.0" 10 | }, 11 | "devDependencies": { 12 | "protocol-buffers": "^4.0.4", 13 | "standard": "^11.0.1" 14 | }, 15 | "scripts": { 16 | "test": "standard", 17 | "protobuf": "protocol-buffers schema.proto -o messages.js" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/mafintosh/hypercore-multicast-swarm.git" 22 | }, 23 | "author": "Mathias Buus (@mafintosh)", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/mafintosh/hypercore-multicast-swarm/issues" 27 | }, 28 | "homepage": "https://github.com/mafintosh/hypercore-multicast-swarm" 29 | } 30 | -------------------------------------------------------------------------------- /schema.proto: -------------------------------------------------------------------------------- 1 | message Node { 2 | required uint64 index = 1; 3 | required bytes hash = 2; 4 | required uint64 size = 3; 5 | } 6 | 7 | message Message { 8 | repeated Node nodes = 1; 9 | optional bytes signature = 2; 10 | required uint64 seq = 3; 11 | optional bytes data = 4; 12 | } 13 | --------------------------------------------------------------------------------