grenache-grape
Version:
Granache Grape - DHT for micro-services
372 lines (293 loc) • 7.9 kB
JavaScript
'use strict'
const DHT = require('bittorrent-dht')
const records = require('record-cache')
const _ = require('./utils')
const CbQ = require('cbq')
const async = require('async')
const http = require('http')
const Events = require('events')
const debug = require('debug')('grenache:grape')
const getRawBody = require('raw-body')
const noop = () => { }
class Grape extends Events {
constructor (conf) {
super()
this.conf = {
host: null,
dht_maxTables: 5000,
dht_maxValues: 5000,
dht_port: 20001,
dht_bootstrap: [],
dht_concurrency: 24,
dht_nodeLiveness: 5 * 60 * 1000, // 5 min
api_port: null,
dht_peer_maxAge: 120000,
check_maxPayloadSize: 8192,
...conf
}
this._mem = records({
maxSize: 5000,
maxAge: Math.ceil(this.conf.dht_peer_maxAge / 2)
})
this.cbq0 = new CbQ()
this._interface = {}
this._active = false
}
createNode (cb) {
const dht = new DHT({
maxTables: this.conf.dht_maxTables,
maxValues: this.conf.dht_maxValues,
host: this.conf.host || false,
bootstrap: this.conf.dht_bootstrap,
timeBucketOutdated: this.conf.dht_nodeLiveness,
verify: this.conf.verify,
maxAge: this.conf.dht_peer_maxAge
})
dht.on('announce', (_peer, ih) => {
const val = this.hex2str(ih)
debug(this.conf.dht_port, 'announce', val)
this.emit('announce', val)
})
dht.on('warning', () => {
debug(this.conf.dht_port, 'warning')
this.emit('warning')
})
dht.on('node', () => {
debug(this.conf.dht_port, 'node')
this.emit('node')
})
dht.on('listening', () => {
debug(this.conf.dht_port, 'listening')
this.emit('listening')
})
dht.on('ready', () => {
debug(this.conf.dht_port, 'ready')
this.emit('ready')
})
dht.on('error', (err) => {
debug(this.conf.dht_port, 'error', err)
})
dht.on('peer', (_peer, val, from) => {
const ih = this.str2hex(val)
const peer = `${_peer.host}:${_peer.port}`
this._mem.add(ih, peer)
debug(this.conf.dht_port, 'found potential peer ' + peer + (from ? ' through ' + from.address + ':' + from.port : '') + ' for hash: ' + val)
this.emit('peer', peer)
})
dht.once('error', handleBootstrapError)
function handleBootstrapError (err) {
cb(err)
}
dht.listen(this.conf.dht_port, (err) => {
dht.removeListener('error', handleBootstrapError)
if (err) return cb(err)
cb(null, dht)
})
}
hex2str (val) {
return Buffer.from(val, 'hex').toString()
}
str2hex (val) {
return Buffer.from(val).toString('hex')
}
str2buf (val, enc) {
try {
return Buffer.from(JSON.parse(val))
} catch (ex) {
return Buffer.from(val, enc || '')
}
}
onRequest (type, data, cb) {
const met = `handlePeer${_.upperFirst(_.camelCase(`-${type}`))}`
if (!this[met]) {
return cb(new Error('ERR_REQ_NOTFOUND'))
}
this[met](data, cb)
}
handlePeerLookup (_val, cb) {
if (!_val || typeof _val !== 'string') {
return cb(new Error('ERR_GRAPE_LOOKUP'))
}
this.lookup(
_val,
cb
)
}
handlePeerAnnounce (data, cb) {
if (!data || !Array.isArray(data)) {
return cb(new Error('ERR_GRAPE_ANNOUNCE'))
}
const [val, port] = [data[0], data[1]]
this.announce(val, port, cb)
}
handlePeerPut (opts, cb) {
if (opts.k) {
if (!Buffer.isBuffer(opts.k)) {
opts.k = this.str2buf(opts.k, 'hex')
}
if (opts.sig && !Buffer.isBuffer(opts.sig)) {
opts.sig = this.str2buf(opts.sig, 'hex')
}
if (opts.salt && !Buffer.isBuffer(opts.salt)) {
opts.salt = this.str2buf(opts.salt)
}
}
this.put(opts, cb)
}
handlePeerGet (hash, cb) {
this.get(hash, cb)
}
announce (val, port, cb) {
if (!Number.isInteger(port)) {
return cb(new Error('ERR_GRAPE_SERVICE_PORT'))
}
cb = cb || noop
this.node.announce(
this.str2hex(val),
port || this.conf.dht_port,
cb
)
}
lookup (val, cb) {
const ih = this.str2hex(val)
this.cbq0.push(ih, (err, data) => {
cb(err, data)
})
const kcnt = this.cbq0.cnt(ih)
if (kcnt > 1) return
this.node.lookup(ih, (err, cnt) => {
debug(`lookup ${val} found ${cnt} nodes`)
if (err) {
return this.cbq0.trigger(ih, err, null)
}
setImmediate(() => {
const peers = this._mem.get(ih, 100)
this.cbq0.trigger(ih, null, peers)
})
})
}
put (opts, cb) {
cb = cb || noop
try {
this.node.put(opts, (err, res) => {
if (err) return cb(err)
cb(null, this.str2hex(res))
})
} catch (e) {
cb(e)
}
}
get (opts, cb) {
if (typeof opts === 'string') opts = this.str2buf(opts, 'hex')
if (Buffer.isBuffer(opts)) opts = { hash: opts }
if (opts.salt && !Buffer.isBuffer(opts.salt)) {
opts.salt = this.str2buf(opts.salt)
}
try {
this.node.get(opts.hash, opts, (err, res) => {
if (!res) {
return cb(err)
}
res.id = this.str2hex(res.id)
res.v = res.v.toString()
if (res.k) {
res.k = res.k.toString('hex')
}
if (res.sig) {
res.sig = res.sig.toString('hex')
}
if (res.salt) {
res.salt = res.salt.toString()
}
if (res.token) {
res.token = res.token.toString('hex')
}
cb(err, res)
})
} catch (e) {
let msg = 'ERR_GRAPE_GENERIC'
if (e.message.indexOf('Invalid hex string') > -1) {
msg = 'ERR_GRAPE_HASH_FORMAT'
}
cb(new Error(msg))
}
}
transportHttp (cb) {
if (!this.conf.api_port) return cb(new Error('ERR_NO_PORT'))
const maxPayloadSize = this.conf.check_maxPayloadSize
const handleRequest = (req, rep, msg) => {
try {
msg = JSON.parse(msg)
} catch (e) {
rep.statusCode = 500
rep.end('ERR_GRAPE_PAYLOAD_INVALID')
return
}
const type = req.url.substr(1)
const data = msg.data
this.onRequest(type, data, (err, res) => {
if (err && err.code) {
rep.statusCode = err.code
}
rep.end(JSON.stringify(err ? err.message : res))
})
}
const server = http.createServer((req, res) => {
getRawBody(req, { limit: maxPayloadSize })
.then((buf) => {
handleRequest(req, res, buf.toString())
})
.catch((err) => {
res.statusCode = 500
if (err.type === 'entity.too.large') {
res.statusCode = 413
return res.end('ERR_GRAPE_PAYLOAD_SIZE')
}
res.end(err.message)
})
})
const listenArgs = [this.conf.api_port]
if (this.conf.host) {
listenArgs.push(this.conf.host)
}
listenArgs.push(cb)
server.listen.apply(server, listenArgs)
this._interface.http = server
}
start (cb) {
cb = cb || noop
if (this._active) {
debug('skipping start, since Grape is already active')
return cb()
}
debug('starting')
this.createNode(
(err, node) => {
if (err) return cb(err)
this.node = node
this.transportHttp(err => {
if (err) return cb(err)
this._active = true
cb()
})
}
)
}
stop (cb) {
cb = cb || noop
async.series([
(cb) => this.node ? this.node.destroy(cb) : cb(),
(cb) => {
const httpsSrv = this._interface.http
if (!httpsSrv) return cb()
httpsSrv.close(cb)
}
], (err) => {
delete this.node
delete this._interface.http
this._active = false
cb(err)
})
}
}
module.exports = Grape