├── .gitignore ├── .npmignore ├── .travis.yml ├── package.json ├── LICENSE ├── README.md ├── test.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .travis.yml 2 | test.js 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - lts/* 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "k-rpc-socket", 3 | "version": "1.11.1", 4 | "description": "Low level implementation of the k-rpc network layer that the BitTorrent DHT uses", 5 | "main": "index.js", 6 | "chromeapp": { 7 | "dgram": "chrome-dgram", 8 | "dns": "chrome-dns", 9 | "net": "chrome-net" 10 | }, 11 | "dependencies": { 12 | "bencode": "^2.0.0", 13 | "chrome-dgram": "^3.0.2", 14 | "chrome-dns": "^1.0.0", 15 | "chrome-net": "^3.3.2" 16 | }, 17 | "devDependencies": { 18 | "standard": "*", 19 | "tape": "^4.4.0" 20 | }, 21 | "scripts": { 22 | "test": "standard && tape test.js" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/mafintosh/k-rpc-socket.git" 27 | }, 28 | "author": "Mathias Buus (@mafintosh)", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/mafintosh/k-rpc-socket/issues" 32 | }, 33 | "homepage": "https://github.com/mafintosh/k-rpc-socket" 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 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 | # k-rpc-socket 2 | 3 | Low level implementation of the k-rpc network layer that the [BitTorrent DHT](http://www.bittorrent.org/beps/bep_0005.html) uses. 4 | Mostly extracted from the [bittorrent-dht](https://github.com/feross/bittorrent-dht) module on npm into its own repo. 5 | 6 | ``` 7 | npm install k-rpc-socket 8 | ``` 9 | 10 | [![build status](http://travis-ci.org/mafintosh/k-rpc-socket.svg?branch=master)](http://travis-ci.org/mafintosh/k-rpc-socket) 11 | 12 | ## Usage 13 | 14 | ``` js 15 | var rpc = require('k-rpc-socket') 16 | 17 | var socket = rpc() 18 | 19 | socket.on('query', function (query, peer) { 20 | socket.response(peer, query, {echo: query.a}) 21 | }) 22 | 23 | socket.bind(10000, function () { 24 | var anotherSocket = rpc() 25 | anotherSocket.query({host: '127.0.0.1', port: 10000}, {q: 'echo', a: {hello: 'world'}}, function (err, response) { 26 | console.log(response.r) // prints {echo: {hello: Buffer('world')}} 27 | }) 28 | }) 29 | ``` 30 | 31 | ## API 32 | 33 | #### `var socket = rpc([options])` 34 | 35 | Create a new k-rpc-socket. Options include: 36 | 37 | ``` js 38 | { 39 | timeout: queryTimeout, // defaults to 2s 40 | socket: optionalUdpSocket, 41 | isIP: optionalBooleanFunction 42 | } 43 | ``` 44 | 45 | #### `socket.send(peer, message, [callback])` 46 | 47 | Send a raw message. The callback is called when the message has been flushed from the socket. 48 | 49 | #### `var id = socket.query(peer, query, [callback])` 50 | 51 | Send a query message. The callback is called with `(err, response, peer, request)`. 52 | You should set the method name you are trying to call as `{q: 'method_name'}` and query data as `{a: someQueryData}`. 53 | 54 | The query method returns a query id. You can use this id to cancel the query using the `.cancel` method. 55 | 56 | #### `socket.cancel(id)` 57 | 58 | Cancel a query. Will call the corresponding query's callback with an error indicating that it was cancelled. 59 | 60 | #### `socket.response(peer, query, response, [callback])` 61 | 62 | Send a response to a query. The callback is called when the message has been flushed from the socket. 63 | 64 | #### `socket.error(peer, query, error, [callback])` 65 | 66 | Send an error reploy to a query. The callback is called when the message has been flushed from the socket. 67 | 68 | #### `socket.inflight` 69 | 70 | Integer representing the number of concurrent queries that are currently pending. 71 | 72 | #### `socket.destroy()` 73 | 74 | Destroys and unbinds the socket 75 | 76 | #### `socket.bind([port], [address], [callback])` 77 | 78 | Call this to bind to a specific port. If port is not specified or is 0, the operating system 79 | will attempt to bind to a random port. If address is not specified, the operating system will 80 | attempt to listen on all addresses. 81 | 82 | If you don't call this a random free port will be chosen. 83 | 84 | #### `socket.on('query', query, peer)` 85 | 86 | When a query is received a `query` event is emitted with the query data and a peer object representing the querying peer. 87 | 88 | #### `socket.on('warning', error)` 89 | 90 | Emitted when a non fatal error has occured. It is safe to ignore this. 91 | 92 | #### `socket.on('error', error)` 93 | 94 | Emitted when a fatal error has occured. 95 | 96 | ## License 97 | 98 | MIT 99 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var rpc = require('./') 3 | var dgram = require('dgram') 4 | 5 | tape('query + response', function (t) { 6 | var server = rpc() 7 | var queried = false 8 | 9 | server.on('query', function (query, peer) { 10 | queried = true 11 | t.same(peer.address, '127.0.0.1') 12 | t.same(query.q.toString(), 'hello_world') 13 | t.same(query.a, { hej: 10 }) 14 | server.response(peer, query, { hello: 42 }) 15 | }) 16 | 17 | server.bind(0, function () { 18 | var port = server.address().port 19 | var client = rpc() 20 | t.same(client.inflight, 0) 21 | client.query({ host: '127.0.0.1', port: port }, { q: 'hello_world', a: { hej: 10 } }, function (err, res) { 22 | t.same(client.inflight, 0) 23 | server.destroy() 24 | client.destroy() 25 | t.error(err) 26 | t.ok(queried) 27 | t.same(res.r, { hello: 42 }) 28 | t.end() 29 | }) 30 | t.same(client.inflight, 1) 31 | }) 32 | }) 33 | 34 | tape('parallel query', function (t) { 35 | var server = rpc() 36 | 37 | server.on('query', function (query, peer) { 38 | server.response(peer, query, { echo: query.a }) 39 | }) 40 | 41 | server.bind(0, function () { 42 | var port = server.address().port 43 | var client = rpc() 44 | var peer = { host: '127.0.0.1', port: port } 45 | 46 | client.query(peer, { q: 'echo', a: 1 }, function (_, res) { 47 | t.same(res.r, { echo: 1 }) 48 | done() 49 | }) 50 | client.query(peer, { q: 'echo', a: 2 }, function (_, res) { 51 | t.same(res.r, { echo: 2 }) 52 | done() 53 | }) 54 | 55 | t.same(client.inflight, 2) 56 | 57 | function done () { 58 | if (client.inflight) return 59 | client.destroy() 60 | server.destroy() 61 | t.end() 62 | } 63 | }) 64 | }) 65 | 66 | tape('query + error', function (t) { 67 | var server = rpc() 68 | 69 | server.on('query', function (query, peer) { 70 | server.error(peer, query, 'oh no') 71 | }) 72 | 73 | server.bind(0, function () { 74 | var port = server.address().port 75 | var client = rpc() 76 | client.query({ host: '127.0.0.1', port: port }, { q: 'hello_world', a: { hej: 10 } }, function (err) { 77 | client.destroy() 78 | server.destroy() 79 | t.ok(err) 80 | t.same(err.message, 'oh no') 81 | t.end() 82 | }) 83 | }) 84 | }) 85 | 86 | tape('timeout', function (t) { 87 | var socket = rpc({ timeout: 100 }) 88 | 89 | socket.query({ host: 'example.com', port: 12345 }, { q: 'timeout' }, function (err) { 90 | socket.destroy() 91 | t.ok(err) 92 | t.same(err.message, 'Query timed out') 93 | t.end() 94 | }) 95 | }) 96 | 97 | tape('do not crash on empty string', function (t) { 98 | if (/^v0\.10\./.test(process.version)) { 99 | // Sending a zero length udp message does not work on Node 0.10 100 | t.pass('skipping test on Node 0.10') 101 | t.end() 102 | return 103 | } 104 | var server = rpc() 105 | var socket = dgram.createSocket('udp4') 106 | 107 | server.on('query', function (query, peer) { 108 | t.fail('should not get a query') 109 | }) 110 | 111 | server.on('warning', function (err) { 112 | t.ok(err instanceof Error, 'got expected warning') 113 | server.destroy() 114 | socket.close() 115 | t.end() 116 | }) 117 | 118 | server.bind(0, function () { 119 | var port = server.address().port 120 | socket.send('' /* invalid bencoded data */, 0, 0, port, '127.0.0.1') 121 | }) 122 | }) 123 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var dgram = require('dgram') 2 | var bencode = require('bencode') 3 | var isIP = require('net').isIP 4 | var dns = require('dns') 5 | var util = require('util') 6 | var events = require('events') 7 | 8 | var ETIMEDOUT = new Error('Query timed out') 9 | ETIMEDOUT.code = 'ETIMEDOUT' 10 | 11 | var EUNEXPECTEDNODE = new Error('Unexpected node id') 12 | EUNEXPECTEDNODE.code = 'EUNEXPECTEDNODE' 13 | 14 | module.exports = RPC 15 | 16 | function RPC (opts) { 17 | if (!(this instanceof RPC)) return new RPC(opts) 18 | if (!opts) opts = {} 19 | 20 | var self = this 21 | 22 | this.timeout = opts.timeout || 2000 23 | this.inflight = 0 24 | this.destroyed = false 25 | this.isIP = opts.isIP || isIP 26 | this.socket = opts.socket || dgram.createSocket('udp4') 27 | this.socket.on('message', onmessage) 28 | this.socket.on('error', onerror) 29 | this.socket.on('listening', onlistening) 30 | 31 | this._tick = 0 32 | this._ids = [] 33 | this._reqs = [] 34 | this._timer = setInterval(check, Math.floor(this.timeout / 4)) 35 | 36 | events.EventEmitter.call(this) 37 | 38 | function check () { 39 | var missing = self.inflight 40 | if (!missing) return 41 | for (var i = 0; i < self._reqs.length; i++) { 42 | var req = self._reqs[i] 43 | if (!req) continue 44 | if (req.ttl) req.ttl-- 45 | else self._cancel(i, ETIMEDOUT) 46 | if (!--missing) return 47 | } 48 | } 49 | 50 | function onlistening () { 51 | self.emit('listening') 52 | } 53 | 54 | function onerror (err) { 55 | if (err.code === 'EACCES' || err.code === 'EADDRINUSE') self.emit('error', err) 56 | else self.emit('warning', err) 57 | } 58 | 59 | function onmessage (buf, rinfo) { 60 | if (self.destroyed) return 61 | if (!rinfo.port) return // seems like a node bug that this is nessesary? 62 | 63 | try { 64 | var message = bencode.decode(buf) 65 | } catch (e) { 66 | return self.emit('warning', e) 67 | } 68 | 69 | var type = message && message.y && message.y.toString() 70 | 71 | if (type === 'r' || type === 'e') { 72 | if (!Buffer.isBuffer(message.t)) return 73 | 74 | try { 75 | var tid = message.t.readUInt16BE(0) 76 | } catch (err) { 77 | return self.emit('warning', err) 78 | } 79 | 80 | var index = self._ids.indexOf(tid) 81 | if (index === -1 || tid === 0) { 82 | self.emit('response', message, rinfo) 83 | self.emit('warning', new Error('Unexpected transaction id: ' + tid)) 84 | return 85 | } 86 | 87 | var req = self._reqs[index] 88 | if (req.peer.host !== rinfo.address) { 89 | self.emit('response', message, rinfo) 90 | self.emit('warning', new Error('Out of order response')) 91 | return 92 | } 93 | 94 | self._ids[index] = 0 95 | self._reqs[index] = null 96 | self.inflight-- 97 | 98 | if (type === 'e') { 99 | var isArray = Array.isArray(message.e) 100 | var err = new Error(isArray ? message.e.join(' ') : 'Unknown error') 101 | err.code = isArray && message.e.length && typeof message.e[0] === 'number' ? message.e[0] : 0 102 | req.callback(err, message, rinfo, req.message) 103 | self.emit('update') 104 | self.emit('postupdate') 105 | return 106 | } 107 | 108 | var rid = message.r && message.r.id 109 | if (req.peer && req.peer.id && rid && !req.peer.id.equals(rid)) { 110 | req.callback(EUNEXPECTEDNODE, null, rinfo) 111 | self.emit('update') 112 | self.emit('postupdate') 113 | return 114 | } 115 | 116 | req.callback(null, message, rinfo, req.message) 117 | self.emit('update') 118 | self.emit('postupdate') 119 | self.emit('response', message, rinfo) 120 | } else if (type === 'q') { 121 | self.emit('query', message, rinfo) 122 | } else { 123 | self.emit('warning', new Error('Unknown type: ' + type)) 124 | } 125 | } 126 | } 127 | 128 | util.inherits(RPC, events.EventEmitter) 129 | 130 | RPC.prototype.address = function () { 131 | return this.socket.address() 132 | } 133 | 134 | RPC.prototype.response = function (peer, req, res, cb) { 135 | this.send(peer, { t: req.t, y: 'r', r: res }, cb) 136 | } 137 | 138 | RPC.prototype.error = function (peer, req, error, cb) { 139 | this.send(peer, { t: req.t, y: 'e', e: [].concat(error.message || error) }, cb) 140 | } 141 | 142 | RPC.prototype.send = function (peer, message, cb) { 143 | var buf = bencode.encode(message) 144 | this.socket.send(buf, 0, buf.length, peer.port, peer.address || peer.host, cb || noop) 145 | } 146 | 147 | // bind([port], [address], [callback]) 148 | RPC.prototype.bind = function () { 149 | this.socket.bind.apply(this.socket, arguments) 150 | } 151 | 152 | RPC.prototype.destroy = function (cb) { 153 | this.destroyed = true 154 | clearInterval(this._timer) 155 | if (cb) this.socket.on('close', cb) 156 | for (var i = 0; i < this._ids.length; i++) this._cancel(i) 157 | this.socket.close() 158 | } 159 | 160 | RPC.prototype.query = function (peer, query, cb) { 161 | if (!cb) cb = noop 162 | if (!this.isIP(peer.host)) return this._resolveAndQuery(peer, query, cb) 163 | 164 | var message = { 165 | t: Buffer.allocUnsafe(2), 166 | y: 'q', 167 | q: query.q, 168 | a: query.a 169 | } 170 | 171 | var req = { 172 | ttl: 4, 173 | peer: peer, 174 | message: message, 175 | callback: cb 176 | } 177 | 178 | if (this._tick === 65535) this._tick = 0 179 | var tid = ++this._tick 180 | 181 | var free = this._ids.indexOf(0) 182 | if (free === -1) free = this._ids.push(0) - 1 183 | this._ids[free] = tid 184 | while (this._reqs.length < free) this._reqs.push(null) 185 | this._reqs[free] = req 186 | 187 | this.inflight++ 188 | message.t.writeUInt16BE(tid, 0) 189 | this.send(peer, message) 190 | return tid 191 | } 192 | 193 | RPC.prototype.cancel = function (tid, err) { 194 | var index = this._ids.indexOf(tid) 195 | if (index > -1) this._cancel(index, err) 196 | } 197 | 198 | RPC.prototype._cancel = function (index, err) { 199 | var req = this._reqs[index] 200 | this._ids[index] = 0 201 | this._reqs[index] = null 202 | if (req) { 203 | this.inflight-- 204 | req.callback(err || new Error('Query was cancelled'), null, req.peer) 205 | this.emit('update') 206 | this.emit('postupdate') 207 | } 208 | } 209 | 210 | RPC.prototype._resolveAndQuery = function (peer, query, cb) { 211 | var self = this 212 | 213 | dns.lookup(peer.host, function (err, ip) { 214 | if (err) return cb(err) 215 | if (self.destroyed) return cb(new Error('k-rpc-socket is destroyed')) 216 | self.query({ host: ip, port: peer.port }, query, cb) 217 | }) 218 | } 219 | 220 | function noop () {} 221 | --------------------------------------------------------------------------------