UNPKG

telehash

Version:

A telehash library for node and browserify

518 lines (425 loc) 14 kB
var crypto = require('crypto'); var e3x = require('e3x'); var hashname = require('hashname'); var base32 = hashname.base32; var lob = require('lob-enc'); var stringify = require('json-stable-stringify'); var TLink = require('./link.class'); var Pipe = require('./pipe.class'); var urilib = require('./util/uri'); var handshakelib = require("./util/handshake"); var receive = require('./util/receive'); var loadMeshJSON = require('./util/json').loadMeshJSON; module.exports = Mesh; var log = require("./util/log")("Mesh"); //TODO: this might belong in E3X function normalizeArgs(id){ var keys = id.keys; var pairs = {}; Object.keys(id.keys).forEach(function forEachCSID(csid){ var pair = pairs[csid] = {}; // flexible buffer or base32 input pair.key = Buffer.isBuffer(id.keys[csid]) ? id.keys[csid] : base32.decode(id.keys[csid]); pair.secret = Buffer.isBuffer(id.secrets[csid]) ? id.secrets[csid] : base32.decode(id.secrets[csid]); }); return {pairs: pairs, debug: log.debug}; } function createE3X(args){ var opts = normalizeArgs(args.id); return e3x.self(opts); } /** * @typedef Mesh * @class Mesh - Telehash Mesh Node * @constructor * @param {meshArgs} args - a hash of options for mesh initialization * @param {function} callback */ function Mesh (args, cbMesh, telehash){ cbMesh = cbMesh || cbDefault; // stub for sync usage if(typeof args != 'object' || typeof args.id != 'object') return cbMesh('invalid args, requires id'); this.lib = args.lib || telehash; this.hashname = hashname.fromKeys(args.id.keys); this.log = log; this.args = args; this.keys = args.id.keys; this.linkedhash = null; this.extended = []; this.extensions = []; this.index = {}; // track by hashname and exchange token this.links = []; // easy array of all this.routes = {}; // routed token => pipe mapping this.routers = []; // default routers // track any current public addresses per-mesh this.json_store = {}; this.linked_cb = function Mesh_linked_cb_noop(){}; this.public = { ipv4 : args.ipv4 || null, ipv6 : args.ipv6 || null, url : args.url || null }; this.indexer = Promise.resolve(); // convert all id keys/secrets to pairs for e3x this.self = createE3X(args); if(!this.self) return cbMesh(e3x.err); args.id = null; // load our own json id loadMeshJSON(this, this.hashname,{keys:this.keys}); // after extensions have run, load any other arguments function ready(err, mesh) { // links can be passed in if(Array.isArray(args.links)) args.links.forEach(function forEachLinkArg(linkArg){ if(linkArg.hashname == mesh.hashname) return; // ignore ourselves, happens mesh.link(linkArg); }); cbMesh(err,mesh); } // next, iterate load any/all extensions, callback when fully done var extboot = this.args.extensions || telehash.extensions; var todo = Object.keys(extboot); if(!todo.length) ready(null, this); // run them all in parallel so that synchronous ones aren't blocked var done = 0; var error; var mesh = this; todo.forEach(function (ext){ mesh.extend(extboot[ext], function forEachExtension(err){ error = error || err; if(++done == todo.length) ready(error, mesh); }); }); return this; } Mesh.log = function Mesh_log(args){ return log; } /** * @return {object} A shareable json id for this mesh. */ Mesh.prototype.json = function Mesh_json() { var json = this.json_store[this.hashname]; json.paths = this.paths(); // dynamic return json; } /** * @return {string} A shareable link uri for this mesh. */ Mesh.prototype.uri = function Mesh_uri(uri) { uri = uri || "link://"; uri = urilib.decode(uri); uri.hostname = uri.hostname || this.public.ipv4; // go through all paths to get best var json = this.json(); if (!(uri.hostname && uri.port)){ var paths = json.paths; for (var i in paths ){ if (uri.hostname && uri.port) break; uri.hostname = uri.hostname || paths[i].ip; uri.port = uri.port || paths[i].port; } } uri.hostname = uri.hostname || '127.0.0.1'; return urilib.keys(uri, json.keys); }; /** * @param {function=} callback - for change events * @return {string} json string of all link info normalized. */ Mesh.prototype.linked = function Mesh_linked(cb) //TODO: convert Mesh to inherit from eventEmitter { // if given a callback, use that on any change if(typeof cb == 'function') { this.linked_cb = cb; this.linked_hash = ''; // dirty the cache } var all = []; this.links.forEach(function ForEachLink(link){ all.push(link.json); }); all.push(this.json()); var ret = stringify(all, {space:2}); var hash = crypto.createHash('sha256').update(ret).digest('hex'); if(hash !== this.linked_hash) { this.linked_hash = hash; this.linked_cb(all, ret); } return ret; }; /** on-demand extender for the mesh * @param {extension} extended - a telehash extension (implimentors notes coming soon) */ Mesh.prototype.extending = function Mesh_extending(extended) { if(this.extended.indexOf(extended) >= 0) return; this.extended.push(extended); // any current links this.links.forEach(function ForEachLink(link){ link.extend(extended); }); }; // so we only run an extension once Mesh.prototype.extend = function Mesh_extend(ext, cbExtend){ // callback is optional cbExtend = cbExtend || cbDefault; // only do once per mesh if(this.extensions.indexOf(ext) >= 0) return cbExtend(); log.debug('extending mesh with',ext.name); this.extensions.push(ext); if(typeof ext.mesh === 'function') { // give it a chance to fill in and set up var mesh = this; ext.mesh(mesh, function meshExtensionInstallCB(err, extended){ if(extended) { extended.name = ext.name; // enforce mesh.extending(extended); } cbExtend(err, extended); }); } else { return cbExtend(); } }; /** * Main routing to handle incoming packets from any transport * @param {Buffer} packet the raw packet buffer * @param {pipe} pipe a telehash transport pipe */ Mesh.prototype.receive = function Mesh_receive(packet, pipe) { packet = receive.decloak(packet, pipe); if (packet instanceof Error) return log.debug(packet); var type = receive.type(packet, pipe); log.debug(this.hashname.substr(0,8) + ': incoming '+ type +' packet; ' + packet.length + " bytes over " + pipe.type); if(type === "channel") return receive.channel(this, packet, pipe); else if (type === "handshake") return receive.handshake(this, packet, pipe); else if (type === "hn") return this.discovered({keys : JSON.parse(packet.head).json.hn, pipe:pipe}); else if (type instanceof Error) return log.debug(type); }; /** * collect incoming handshakes to accept them * @param {object} id * @param {handshake} handshake * @param {pipe} pipe * @param {Buffer} message */ Mesh.prototype.handshake = function Mesh_handshake(id, handshake, pipe, message) { var val = false; //decode and validate handshake = handshakelib.bootstrap(handshake); if (handshake && handshakelib.validate(id, handshake, message, this)){ // get all from cache w/ matching at, by type var types = handshakelib.types(handshake, id); if (types.link){ var from = handshakelib.from(handshake, pipe, types.link); if(this.index[from.hashname]){ from.sync = false; // tell .link to not auto-sync! val = this.link(from); this.index[from.hashname.emit('status', null, link)] } else { log.debug('untrusted hashname',from); from.received = {packet:types.link._message, pipe:pipe}; // optimization hints as link args from.handshake = types; // include all handshakes if(this.accept) this.accept(from); } } else { // bail if no link log.debug('handshakes w/ no link yet', id, types) } } return val; }; // sanitize any discovered data before accepting Mesh.prototype.discovered = function Mesh_discovered(to) { if(!to || typeof to != 'object' || typeof to.keys != 'object') return; log.debug("discovered " + to.hashname) to.hashname = hashname.fromKeys(to.keys); if(!to.hashname) return log.warn('invalid hashname',to.keys); if(to.hashname == this.hashname) return log.debug("ignoring self-discovery"); // can't discover ourselves this.log.debug('processing discovery',to.hashname); to.csid = hashname.match(this.keys,to.keys); if(!to.csid) return mesh.log.warn('invalid json discovered',to); if(this.index[to.hashname]) { this.index[to.hashname].addPipe(to.pipe) this.index[to.hashname].emit('refresh', to.pipe) return; } // already known, but might have changed location, so re-link //finally do this if everythings OK if(this.accept) this.accept(to); }; /** declare wether a link should be used as a router to aid in link creation. * By default, all meshs will perform routing functions. * if the link argument is ommited, this function sets the behaivior of the local node * @param {TLink=} [link=The local Mesh] - the remote link to use as a router * @param {Boolean=} [isRouter=true] - whether to declare the target as a router; */ Mesh.prototype.router = function Mesh_router(link, isRouter) { // just change our router status if(typeof link == 'boolean') { this.json().router = link; this.linked(); // we changed the json! return; } // add link as a router if(typeof link != 'object' || !link.isLink) return log.warn('invald args to mesh.router, not a link',link); isRouter = (isRouter === undefined) ? true : isRouter; // default to true var index = this.routers.indexOf(link); // no longer a default if(!isRouter) { delete link.json.router; if(index >= 0) this.routers.splice(index,1); } else { // add default router to all link.json.router = true; if(index == -1) this.routers.push(link); this.links.forEach(function ForEachLink(link2){ if(link2 != link) link2.addPath({type:'peer',hn:link.hashname}); }); } }; function cbDefault(err, ret){ return (err) ? log.error(err) : ret; } /** Discovery mode enables any network transport to send un-encrypted announcements * to any other endpoints that are available locally only. This can be used to * automatically establish a link to a local peer when there is no other mechanism * to exchange keys, such as when they are offline. * * IMPORTANT: This should be used sparingly, as anything on a local network will * be made aware of the sending hashname. While this is generally very low risk, * it should not be left on by default except in special cases. * @param {Boolean} on - true to enable, false to disable * @param {function} callback - a callback function with any error */ Mesh.prototype.discover = function Mesh_discover(opts, cbDiscover) { cbDiscover = cbDiscover || cbDefault; if(opts === true) opts = {}; log.debug('discovery is ' + (opts?'on':'off')); this.discoverable = opts; // notify all extensions var extensions = this.extended.slice(0); while (extensions.length > 0){ var ext = extensions.shift(); if (typeof ext.discover === "function") ext.discover(opts, function noop(){}); } cbDiscover(); return; }; /** collect current addressible paths for this mesh, if any * @return {Array} paths - an array of @pathJSON objects */ Mesh.prototype.paths = function Mesh_paths() { var ret = []; this.extended.forEach(function ForEachExtension(ext){ if(typeof ext.paths != 'function') return; ext.paths().forEach(function ForEachPath(path){ ret.push(JSON.parse(JSON.stringify(path))); }); }); return ret; }; /** * create or retrieve a link to another mesh (local or remote). Accepts multiple * raw arguments to provide the necessary info to connect to the target mesh. * @param {linkURI|hashname|object} raw - either a @linkURI, @hashname, or @thirdformatname * @param {function=} callback - one time listener for the TLink 'status' event * @return {TLink} */ Mesh.prototype.link = function(rawargs, cbLink) { var args = TLink.ParseArguments(rawargs); if (args instanceof Error || (args.hashname === this.hashname)) return TLink.Bail(args, this, cbLink); // get/create link var link = this.index[args.hashname] || new TLink(this, args, log); // set/update info link.setInfo(args); // link.createExchange(); log.debug("initializing link") // pipes, paths, packets... link.initialize(args); // add callback as a one-time event listener if (typeof cbLink === 'function') link.once('status', cbLink); return link; }; /** utility to bind two meshes together, establishing links and internal pipes * @param {Mesh} target - the mesh to connect to */ Mesh.prototype.mesh = function(target) { var meshA = this; // create virtual pipes var pipeAB = new Pipe('mesh'); var pipeBA = new Pipe('mesh'); // direct pipes to the other side function AB_onSend(packet){target.receive(packet,pipeBA);} function BA_onSend(packet){meshA.receive(packet,pipeAB);} pipeAB.on('send',function(context,a){ log.debug(meshA.hashname.substr(0,8) + " sending to " + target.hashname.substr(0,8)) AB_onSend(a); }); pipeBA.on('send',function(context,a){ log.debug(target.hashname.substr(0,8) + " sending to " + meshA.hashname.substr(0,8)) BA_onSend(a); }); return new Promise(function(resolve,reject){ // create links both ways var linkAB = meshA.link({keys:target.keys}); var linkBA = target.link({keys:meshA.keys}); // add the internal pipes linkAB.addPipe(pipeAB); linkBA.addPipe(pipeBA); var done = false; function resolver(){ if (done) resolve() else done = true; } linkAB.status(resolver) linkBA.status(resolver) }) };