UNPKG

ferment

Version:

Peer-to-peer audio publishing and streaming application. Like SoundCloud but decentralized. A mashup of ssb, webtorrent and electron.

218 lines (191 loc) 7.31 kB
// FROM: https://github.com/ssbc/scuttlebot/blob/master/plugins/invite.js var crypto = require('crypto') var ssbKeys = require('ssb-keys') var cont = require('cont') var explain = require('explain-error') var ip = require('ip') var mdm = require('mdmanifest') var valid = require('scuttlebot/lib/validators') var apidoc = require('scuttlebot/lib/apidocs').invite var ref = require('ssb-ref') var ssbClient = require('ssb-client') // invite plugin // adds methods for producing invite-codes, // which peers can use to command your server to follow them. function isFunction (f) { return 'function' === typeof f } function isString (s) { return 'string' === typeof s } function isObject (o) { return o && 'object' === typeof o } module.exports = { name: 'invite', version: '1.0.0', manifest: mdm.manifest(apidoc), permissions: { master: {allow: ['create']} // temp: {allow: ['use']} }, init: function (server, config) { var codes = {} var codesDB = server.sublevel('codes') var scope = (config.friends || {}).scope var createClient = this.createClient // add an auth hook. server.auth.hook(function (fn, args) { var pubkey = args[0], cb = args[1] // run normal authentication fn(pubkey, function (err, auth) { if (err || auth) return cb(err, auth) // if no rights were already defined for this pubkey // check if the pubkey is one of our invite codes codesDB.get(pubkey, function (_, code) { // disallow if this invite has already been used. if (code && (code.used >= code.total)) cb() else cb(null, code && code.permissions) }) }) }) return { create: valid.async(function (n, cb) { var modern = false if (isObject(n) && n.modern) { n = 1 modern = true } var addr = server.getAddress() var host = ref.parseAddress(addr).host if (!config.allowPrivate && (ip.isPrivate(host) || 'localhost' === host)) return cb(new Error('Server has no public ip address, ' + 'cannot create useable invitation')) // this stuff is SECURITY CRITICAL // so it should be moved into the main app. // there should be something that restricts what // permissions the plugin can create also: // it should be able to diminish it's own permissions. // generate a key-seed and its key var seed = crypto.randomBytes(32) var keyCap = ssbKeys.generate('ed25519', seed) // store metadata under the generated pubkey var owner = server.id codesDB.put(keyCap.id, { id: keyCap.id, total: +n, used: 0, permissions: {allow: ['invite.use', 'getAddress'], deny: null} }, function (err) { // emit the invite code: our server address, plus the key-seed if (err) cb(err) else if (modern && server.ws && server.ws.getAddress) { cb(null, server.ws.getAddress() + ':' + seed.toString('base64')) } else { addr = ref.parseAddress(addr) cb(null, [addr.host, addr.port, addr.key].join(':') + '~' + seed.toString('base64')) } }) }, 'number|object'), use: valid.async(function (req, cb) { var rpc = this // fetch the code codesDB.get(rpc.id, function (err, invite) { if (err) return cb(err) // check if we're already following them server.friends.all('follow', function (err, follows) { if (follows && follows[server.id] && follows[server.id][req.feed]) return cb(new Error('already following')) // although we already know the current feed // it's included so that request cannot be replayed. if (!req.feed) return cb(new Error('feed to follow is missing')) if (invite.used >= invite.total) return cb(new Error('invite has expired')) invite.used ++ // never allow this to be used again if (invite.used >= invite.total) { invite.permissions = {allow: [], deny: null} } // TODO // okay so there is a small race condition here // if people use a code massively in parallel // then it may not be counted correctly... // this is not a big enough deal to fix though. // -dominic // update code metadata codesDB.put(rpc.id, invite, function (err) { server.emit('log:info', ['invite', rpc.id, 'use', req]) // follow the user server.publish({ type: 'contact', contact: req.feed, scope: typeof req.scope === 'string' ? req.scope : undefined, following: true, pub: true }, cb) }) }) }) }, 'object'), accept: valid.async(function (invite, cb) { // remove surrounding quotes, if found if (invite.charAt(0) === '"' && invite.charAt(invite.length - 1) === '"') invite = invite.slice(1, -1) var opts // connect to the address in the invite code // using a keypair generated from the key-seed in the invite code var modern = false if (ref.isInvite(invite)) { // legacy ivite if (ref.isLegacyInvite(invite)) { var parts = invite.split('~') opts = ref.parseAddress(parts[0])// .split(':') // convert legacy code to multiserver invite code. invite = 'net:' + opts.host + ':' + opts.port + '~shs:' + opts.key.slice(1, -8) + ':' + parts[1] } else modern = true } opts = ref.parseAddress(ref.parseInvite(invite).remote) ssbClient(null, { remote: invite, manifest: {invite: {use: 'async'}, getAddress: 'async'} }, function (err, rpc) { if (err) return cb(explain(err, 'could not connect to server')) // command the peer to follow me rpc.invite.use({ feed: server.id, scope }, function (err, msg) { if (err) return cb(explain(err, 'invite not accepted')) // follow and announce the pub cont.para([ server.publish({ type: 'contact', following: true, autofollow: true, scope: scope, contact: opts.key }), ( opts.host ? server.publish({ type: 'pub', address: opts }) : function (cb) { cb() } ) ])(function (err, results) { if (err) return cb(err) rpc.getAddress(function (err, addr) { rpc.close() // ignore err if this is new style invite if (modern && err) return cb(err, addr) if (server.gossip) server.gossip.add(addr, 'seed') cb(null, results) }) }) }) }) }, 'string') } } }