├── .gitignore ├── README.md ├── index.js ├── package.json ├── screencast-2.gif └── screencast.gif /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | nodeenv 3 | .*.swp 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Peer-to-peer networking for shell scripts & OS pipes. 2 | 3 | npm install chr15m/dreamtime 4 | 5 | ![Screencast of dreamtime connecting to two servers and local](./screencast.gif) 6 | 7 | ### Run 8 | 9 | ./node_modules/.bin/dreamtime unique-room-identifier 10 | 11 | Example: aggregate the output from `ping` from different servers: 12 | 13 | `ping -n wikipedia.org | stdbuf -oL cut -b15- | dreamtime ping-party` 14 | 15 | ![Screencast of dreamtime aggregating script output](./screencast-2.gif) 16 | 17 | For more options & help run `dreamtime` with no arguments. 18 | 19 | ### Node module 20 | 21 | You can `require` dreamtime as a node module: 22 | 23 | // connect to a room with a callback for received messages 24 | room = require('dreamtime')("my-room-id", console.log); 25 | 26 | // our unique fingerprint 27 | console.log(c.client.fingerprint); 28 | 29 | // wait 5 seconds & send a message to the room 30 | // and then disconnect 31 | setTimeout(function() { 32 | room.send("my first test message"); 33 | room.disconnect(); 34 | }, 5000); 35 | 36 | You can also re-use an existing webtorrent client: 37 | 38 | var dreamtime = require("dreamtime"); 39 | room = dreamtime("my-room-id", {"torrent_client": wt}, console.log); 40 | 41 | ### Implementation 42 | 43 | Dreamtime is built on top of [WebTorrent](https://webtorrent.io/) and uses the Bittorrent extension protocol for messaging. 44 | 45 | Discovery is achieved by seeding a torrent with the following `info` field. 46 | 47 | Contents are bencoded and SHA1'ed to make the infoHash as is standard. 48 | 49 | {"length": 1, 50 | "name: YOUR-ROOM-NAME, 51 | "piece length": 16384, 52 | "pieces": 0x5b a9 3c 9d b0 cf f9 3f 52 b5 21 d7 42 0e 43 f6 ed a2 78 4f } 53 | 54 | The content of the torrent is a single character `\0` and so the `pieces` field contains `sha1("\0")`. 55 | 56 | ### Security note 57 | 58 | Channels are completely public. Anybody can join or eavesdrop. Don't share secrets over them. 59 | 60 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var EXT = "dt_share"; 4 | 5 | var WebTorrent = require('webtorrent'); 6 | var bencode = require('bencode'); 7 | var nacl = require('tweetnacl'); 8 | var ripe = require('ripemd160'); 9 | var debug = require('debug')('dreamtime'); 10 | 11 | // check if a string is probably a 32 byte hex representation 12 | var seedregex = /\b[0-9A-F]{64}\b/gi; 13 | 14 | // CLI main function 15 | 16 | function main(args) { 17 | var name = args.name; 18 | var client = make_client(args); 19 | console.log("me\t", client.fingerprint); 20 | // handle data from peers to stdout 21 | listen(client, name, post_to_stdout); 22 | // send stdin to peers 23 | attach_readline_interface(function(type, data) { 24 | if (type == "line") { 25 | send(client, data, post_to_stdout); 26 | } else if (type == "exit") { 27 | console.log("exiting"); 28 | } 29 | }); 30 | } 31 | 32 | function post_to_stdout() { 33 | var args = Array.prototype.slice.call(arguments); 34 | args[0] = args[0] + "\t"; 35 | console.log.apply(console, args); 36 | } 37 | 38 | // client datastructure 39 | 40 | function make_client(opts) { 41 | var opts = opts || {}; 42 | var struct = { 43 | // peer connections 44 | "wires": [], 45 | // messages already seen recently 46 | "seen": [], 47 | "seenptr": 0, 48 | }; 49 | // how many recent messages to detect repeats (rinngbuffer) 50 | struct.seen.length = 1024; 51 | // webtorrent client 52 | struct.torrent_client = opts.torrent_client || new WebTorrent(); 53 | // nacl key pair 54 | struct.keys = opts.keys || nacl.sign.keyPair(); 55 | // compute my pk and fingerprint 56 | struct.pk = Buffer(struct.keys.publicKey); 57 | struct.fingerprint = fingerprint_key(struct.pk); 58 | return struct; 59 | } 60 | 61 | // crypto & utility functions 62 | 63 | function fingerprint_key(pk) { 64 | return new ripe().update(Buffer(pk)).digest('hex'); 65 | } 66 | 67 | function make_packet(payload, keys) { 68 | var packet = {k: Buffer(keys.publicKey), u: Buffer(nacl.randomBytes(20)), p: Buffer(payload.toString())}; 69 | packet.s = Buffer(nacl.sign.detached(Buffer(packet.k + packet.u + packet.p), keys.secretKey)); 70 | return packet; 71 | } 72 | 73 | function process_received_packet(client, packet, wire) { 74 | var verified = nacl.sign.detached.verify(Buffer(packet.k + packet.u + packet.p), new Uint8Array(packet.s), new Uint8Array(packet.k)); 75 | debug("verified:", verified); 76 | debug("packet:", packet); 77 | if (verified) { 78 | var uid = packet.k + packet.u; 79 | // check if this is a repeat packet 80 | if (client.seen.indexOf(uid) == -1) { 81 | client.seen[client.seenptr] = uid; 82 | client.seenptr = (client.seenptr + 1) % client.seen.length; 83 | client.wires.map(function(w) { 84 | if (w != wire) { 85 | w.extended(EXT, packet); 86 | } 87 | }); 88 | return ["msg", fingerprint_key(packet.k), packet["p"].toString()]; 89 | } else { 90 | debug("ignoring repeat packet"); 91 | } 92 | } 93 | } 94 | 95 | function send(client, message, cb) { 96 | if (client.torrent) { 97 | var got = process_received_packet(client, make_packet(message, client.keys)); 98 | if (got) { 99 | cb.apply(null, got); 100 | } 101 | } 102 | } 103 | 104 | // interface to bittorrent client 105 | 106 | function attach_bittorrent_extension_protocol(client, wire, addr, cb) { 107 | var t = function(wire) { 108 | wire.extendedHandshake.pk = client.pk; 109 | }; 110 | t.prototype.name = EXT; 111 | t.prototype.onExtendedHandshake = function (handshake) { 112 | if (handshake.m && handshake.m[EXT]) { 113 | wire.fingerprint = fingerprint_key(handshake.pk); 114 | client.wires.push(wire); 115 | cb("peer", wire); 116 | debug("wires:", client.wires.length); 117 | } 118 | } 119 | t.prototype.onMessage = function(message) { 120 | debug("raw:", message); 121 | debug("wire:", wire.fingerprint); 122 | if (wire.fingerprint) { 123 | var packet = bencode.decode(message); 124 | cb("packet", wire, packet); 125 | } 126 | } 127 | return t; 128 | } 129 | 130 | function listen(client, name, cb) { 131 | if (client.torrent) { 132 | disconnect(client); 133 | } 134 | 135 | var content = new Buffer("\0"); 136 | content.name = name; 137 | 138 | client.torrent_client.on('torrent', function(torrent) { 139 | cb("hash", torrent.infoHash); 140 | client.torrent = torrent; 141 | }); 142 | 143 | var torrent = client.torrent_client.seed(content, function (torrent) { 144 | cb("open"); 145 | }); 146 | 147 | torrent.on("wire", function(wire, addr) { 148 | debug("saw wire:", wire.peerId); 149 | wire.use(attach_bittorrent_extension_protocol(client, wire, addr, function(type, wire, packet) { 150 | if (type == "packet") { 151 | var got = process_received_packet(client, packet, wire); 152 | if (got) { 153 | cb.apply(null, got); 154 | } 155 | } else if (type == "peer") { 156 | cb(type, wire.fingerprint); 157 | } 158 | })); 159 | wire.on("close", function() { 160 | wires = client.wires.filter(function(w) { return w != wire; }); 161 | if (wire.fingerprint) { 162 | cb("left", wire.fingerprint); 163 | debug("wires:", client.wires.length); 164 | } 165 | }); 166 | }); 167 | } 168 | 169 | function disconnect(client) { 170 | if (client.torrent) { 171 | client.torrent_client.remove(client.torrent); 172 | client.torrent = null; 173 | } 174 | } 175 | 176 | // manage stdin interface in CLI mode 177 | 178 | function attach_readline_interface(cb) { 179 | var readline = require('readline'); 180 | 181 | var rl = readline.createInterface({ 182 | input: process.stdin, 183 | terminal: false, 184 | }); 185 | 186 | rl.on('line', function(line) { 187 | cb("line", line); 188 | }); 189 | 190 | rl.on('close', function() { 191 | cb("exit"); 192 | process.exit(0); 193 | }); 194 | } 195 | 196 | // node module interface 197 | function connect(room, opts, cb) { 198 | if (typeof(opts) == "function") { 199 | cb = opts; 200 | opts = {}; 201 | } 202 | var c = make_client(opts); 203 | listen(c, room, cb); 204 | return { 205 | "client": c, 206 | "send": function(msg) { 207 | send(c, msg, cb); 208 | }, 209 | "disconnect": function() { 210 | disconnect(c); 211 | } 212 | }; 213 | } 214 | 215 | // extract nacl keypair from the argument 216 | function extractKeys(k) { 217 | var fromSeed = nacl.sign.keyPair.fromSeed; 218 | if (seedregex.exec(k)) { 219 | return fromSeed(Buffer(k, "hex")) 220 | } else { 221 | var fs = require('fs'); 222 | if (fs.existsSync(k)) { 223 | var seed = seedregex.exec(fs.readFileSync(k).toString()); 224 | if (seed) { 225 | return fromSeed(Buffer(seed[0], "hex")); 226 | } 227 | } else if (k) { 228 | console.error("Seed file does not exist:", k); 229 | } 230 | } 231 | } 232 | 233 | // generate a usage message 234 | function usage(argv) { 235 | return "Usage: " + argv[1] + " ROOM-NAME [-k SIGNING-KEY-SEED]\n\n" + 236 | "\tROOM-NAME is any string that you can use for nodes to find eachother.\n\n" + 237 | "\tSIGNING-KEY-SEED is an optional seed to generate ed25519 keys used to sign all messages.\n" + 238 | "\tIt should be either a 32 byte hex string, or a file containing such a hex string.\n" + 239 | "\tE.g. `head -c32 /dev/urandom | sha256sum | cut -f1 -d' '`\n" + 240 | "\tA random key is used by default.\n\n"; 241 | } 242 | 243 | if (typeof(require)!= 'undefined' && require.main == module) { 244 | var argv = require('minimist')(process.argv.slice(2)); 245 | if (argv._.length > 0) { 246 | argv.name = argv._[0]; 247 | argv.keys = extractKeys(argv.k); 248 | main(argv); 249 | } else { 250 | console.log(usage(process.argv)); 251 | } 252 | } else { 253 | // node module defines 254 | module.exports = connect; 255 | } 256 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dreamtime", 3 | "version": "0.1.0", 4 | "dependencies": { 5 | "debug": "^2.6.8", 6 | "minimist": "^1.2.0", 7 | "readline": "^1.3.0", 8 | "ripemd160": "^2.0.1", 9 | "tweetnacl": "^1.0.0", 10 | "webtorrent": "^0.107.6" 11 | }, 12 | "bin": "./index.js" 13 | } 14 | -------------------------------------------------------------------------------- /screencast-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr15m/dreamtime/fc21410ede061a07722f2ace0e47e467a72732e7/screencast-2.gif -------------------------------------------------------------------------------- /screencast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr15m/dreamtime/fc21410ede061a07722f2ace0e47e467a72732e7/screencast.gif --------------------------------------------------------------------------------