secure-scuttlebot classic

move friends, invite, ref inhouse and remove dep warnings

+2 -5
bin.js
··· 50 50 .use(require('ssb-master')) 51 51 .use(require('ssb-gossip')) 52 52 .use(require('ssb-replicate')) 53 - .use(require('ssb-friends')) 53 + .use(require('./plugins/friends')) 54 54 .use(require('ssb-blobs')) 55 - .use(require('ssb-invite')) 55 + .use(require('./plugins/invite')) 56 56 .use(require('ssb-local')) 57 57 .use(require('ssb-logging')) 58 58 .use(require('ssb-query')) ··· 157 157 158 158 159 159 160 - 161 - 162 -
+1 -4
lib/validators.js
··· 1 1 var valid = require('muxrpc-validation') 2 2 var zerr = require('zerr') 3 - var ref = require('ssb-ref') 3 + var ref = require('../plugins/ref') 4 4 5 5 // errors 6 6 var MissingAttr = zerr('Usage', 'Param % must have a .% of type "%"') ··· 226 226 return AttrType(n, 'hops', 'number') 227 227 } 228 228 }) 229 - 230 - 231 - 232 229 233 230 234 231
+4 -1
npm-shrinkwrap.json
··· 28 28 "muxrpcli": "3", 29 29 "mv": "^2.1.1", 30 30 "osenv": "^0.1.5", 31 + "flumeview-reduce": "^1.3.0", 32 + "layered-graph": "^1.1.1", 31 33 "pull-cat": "~1.1.5", 32 34 "pull-file": "^1.0.0", 35 + "pull-flatmap": "0.0.1", 33 36 "pull-many": "~1.0.6", 37 + "pull-notify": "^0.1.1", 34 38 "pull-pushable": "^2.2.0", 35 39 "pull-stream": "^3.6.2", 36 40 "rimraf": "^2.4.2", ··· 41 45 "ssb-config": "^3.2.5", 42 46 "ssb-db": "^20.0.1", 43 47 "ssb-ebt": "^5.6.1", 44 - "ssb-friends": "^4.1.0", 45 48 "ssb-gossip": "^1.1.0", 46 49 "ssb-invite": "^2.1.2", 47 50 "ssb-keys": "^7.1.1",
-2
package.json
··· 52 52 "ssb-config": "^3.2.5", 53 53 "ssb-db": "^20.0.1", 54 54 "ssb-ebt": "^5.6.1", 55 - "ssb-friends": "^4.1.0", 56 55 "ssb-gossip": "^1.1.0", 57 - "ssb-invite": "^2.1.2", 58 56 "ssb-keys": "^7.1.1", 59 57 "ssb-links": "^3.0.10", 60 58 "ssb-local": "^1.0.0",
+38
plugins/friends/contacts.js
··· 1 + var Reduce = require('flumeview-reduce') 2 + var isFeed = require('../ref').isFeed 3 + //track contact messages, follow, unfollow, block 4 + 5 + module.exports = function (sbot, createLayer, config) { 6 + 7 + var layer = createLayer('contacts') 8 + var initial = false 9 + var hops = {} 10 + hops[sbot.id] = 0 11 + var index = sbot._flumeUse('contacts2', Reduce(9, function (g, data) { 12 + if(!g) g = {} 13 + 14 + var from = data.value.author 15 + var to = data.value.content.contact 16 + var value = 17 + data.value.content.blocking || data.value.content.flagged ? -1 : 18 + data.value.content.following === true ? 1 19 + : -2 20 + 21 + if(isFeed(from) && isFeed(to)) { 22 + if(initial) { 23 + layer(from, to, value) 24 + } 25 + g[from] = g[from] || {} 26 + g[from][to] = value 27 + } 28 + return g 29 + })) 30 + 31 + // trigger flume machinery to wait until index is ready, 32 + // otherwise there is a race condition when rebuilding the graph. 33 + index.get(function (err, g) { 34 + initial = true 35 + layer(g || {}) 36 + }) 37 + } 38 +
+77
plugins/friends/help.js
··· 1 + var SourceDest = { 2 + source: { 3 + type: 'FeedId', 4 + description: 'the feed which posted the contact message' 5 + }, 6 + dest: { 7 + type: 'FeedId', 8 + description: 'the feed the contact message pointed at' 9 + } 10 + } 11 + 12 + var HopsOpts = { 13 + start: { 14 + type: 'FeedId', 15 + description: 'feed at which to start traversing graph from, default to your own feed id' 16 + }, 17 + max: { 18 + type: 'number', 19 + description: 'include feeds less than or equal to this number of hops', 20 + } 21 + } 22 + 23 + var StreamOpts = Object.assign( 24 + HopsOpts, { 25 + live: { 26 + type: 'boolean', 27 + description: 'include real time results, defaults to false', 28 + }, 29 + old: { 30 + type: 'boolean', 31 + description: 'include old results, defaults to true' 32 + } 33 + } 34 + ) 35 + 36 + module.exports = { 37 + description: 'track what feeds are following or blocking each other', 38 + commands: { 39 + isFollowing: { 40 + type: 'async', 41 + description: 'check if a feed is following another', 42 + args: SourceDest 43 + }, 44 + isBlocking: { 45 + type: 'async', 46 + description: 'check if a feed is blocking another', 47 + args: SourceDest 48 + }, 49 + hops: { 50 + type: 'async', 51 + description: 'dump the map of hops, show all feeds, and how far away they are from start', 52 + args: HopsOpts 53 + }, 54 + hopStream: { 55 + type: 'source', 56 + description: 'stream real time changes to hops. output is series of `{<FeedId>: <hops>,...}` merging these together will give the output of hops', 57 + args: StreamOpts 58 + }, 59 + 60 + get: { 61 + type: 'async', 62 + description: 'dump internal state of friends plugin, the stored follow graph', 63 + args: {} 64 + }, 65 + stream: { 66 + type: 'source', 67 + description: 'stream real time changes to graph. of hops, output of `get`, followed by {from: <FeedId>, to: <FeedId>: value: true|null|false, where true represents follow, null represents unfollow, and false represents block.', 68 + args: StreamOpts 69 + }, 70 + createFriendStream: { 71 + type: 'source', 72 + description: 'same as `stream`, but output is series of `{id: <FeedId>, hops: <hops>}`', 73 + args: StreamOpts 74 + }, 75 + } 76 + } 77 +
+128
plugins/friends/index.js
··· 1 + 'use strict' 2 + var LayeredGraph = require('layered-graph') 3 + var pull = require('pull-stream') 4 + var isFeed = require('../ref').isFeed 5 + // friends plugin 6 + // methods to analyze the social graph 7 + // maintains a 'follow' and 'flag' graph 8 + 9 + exports.name = 'friends' 10 + exports.version = '1.0.0' 11 + exports.manifest = { 12 + hopStream: 'source', 13 + onEdge: 'sync', 14 + isFollowing: 'async', 15 + isBlocking: 'async', 16 + hops: 'async', 17 + help: 'sync', 18 + // createLayer: 'sync', // not exposed over RPC as returns a function 19 + get: 'async', // classic (previously marked legacy) 20 + createFriendStream: 'source', // classic (previously marked legacy) 21 + stream: 'source' // classic (previously marked legacy) 22 + } 23 + 24 + //mdm.manifest(apidoc) 25 + 26 + exports.init = function (sbot, config) { 27 + var max = config.friends && config.friends.hops || config.replicate && config.replicate.hops || 3 28 + var layered = LayeredGraph({max: max, start: sbot.id}) 29 + 30 + function isFollowing (opts, cb) { 31 + layered.onReady(function () { 32 + var g = layered.getGraph() 33 + cb(null, g[opts.source] && g[opts.source][opts.dest] >= 0) 34 + }) 35 + } 36 + 37 + function isBlocking (opts, cb) { 38 + layered.onReady(function () { 39 + var g = layered.getGraph() 40 + cb(null, Math.round(g[opts.source] && g[opts.source][opts.dest]) == -1) 41 + }) 42 + } 43 + 44 + //opinion: do not authorize peers blocked by this node. 45 + sbot.auth.hook(function (fn, args) { 46 + var self = this 47 + isBlocking({source: sbot.id, dest: args[0]}, function (err, blocked) { 48 + if(blocked) 49 + args[1](new Error('client is blocked')) 50 + else fn.apply(self, args) 51 + }) 52 + }) 53 + 54 + if(!sbot.replicate) 55 + throw new Error('ssb-friends expects a replicate plugin to be available') 56 + 57 + // opinion: replicate with everyone within max hops (max passed to layered above ^) 58 + pull( 59 + layered.hopStream({live: true, old: true}), 60 + pull.drain(function (data) { 61 + if(data.sync) return 62 + for(var k in data) { 63 + sbot.replicate.request(k, data[k] >= 0) 64 + } 65 + }) 66 + ) 67 + 68 + require('./contacts')(sbot, layered.createLayer, config) 69 + 70 + var classic = require('./legacy')(layered) 71 + 72 + //opinion: pass the blocks to replicate.block 73 + setImmediate(function () { 74 + var block = (sbot.replicate && sbot.replicate.block) || (sbot.ebt && sbot.ebt.block) 75 + if(block) { 76 + function handleBlockUnlock(from, to, value) { 77 + if (value === false) block(from, to, true) 78 + else block(from, to, false) 79 + } 80 + pull( 81 + classic.stream({live: true}), 82 + pull.drain(function (contacts) { 83 + if(!contacts) return 84 + 85 + if (isFeed(contacts.from) && isFeed(contacts.to)) { // live data 86 + handleBlockUnlock(contacts.from, contacts.to, contacts.value) 87 + } else { // initial data 88 + for (var from in contacts) { 89 + var relations = contacts[from] 90 + for (var to in relations) 91 + handleBlockUnlock(from, to, relations[to]) 92 + } 93 + } 94 + }) 95 + ) 96 + } 97 + }) 98 + 99 + return { 100 + hopStream: layered.hopStream, 101 + onEdge: layered.onEdge, 102 + isFollowing: isFollowing, 103 + isBlocking: isBlocking, 104 + 105 + // expose createLayer, so that other plugins may express relationships 106 + createLayer: layered.createLayer, 107 + 108 + // classic, debugging 109 + hops: function (opts, cb) { 110 + layered.onReady(function () { 111 + if(isFunction(opts)) 112 + cb = opts, opts = {} 113 + cb(null, layered.getHops(opts)) 114 + }) 115 + }, 116 + help: function () { return require('./help') }, 117 + // classic 118 + get: classic.get, 119 + createFriendStream: classic.createFriendStream, 120 + stream: classic.stream, 121 + } 122 + } 123 + 124 + // helpers 125 + 126 + function isFunction (f) { 127 + return 'function' === typeof f 128 + }
+91
plugins/friends/legacy.js
··· 1 + var FlatMap = require('pull-flatmap') 2 + var pull = require('pull-stream') 3 + var Notify = require('pull-notify') 4 + 5 + module.exports = function (layered) { 6 + 7 + function mapGraph (g, fn) { 8 + var _g = {} 9 + for(var j in g) 10 + for(var k in g[j]) { 11 + _g[j] = _g[j] || {} 12 + _g[j][k] = fn(g[j][k]) 13 + } 14 + return _g 15 + } 16 + 17 + function map(o, fn) { 18 + var _o = {} 19 + for(var k in o) _o[k] = fn(o[k]) 20 + return _o 21 + } 22 + 23 + function toLegacyValue (v) { 24 + //follow and same-as are shown as follow 25 + //-2 is unfollow, -1 is block. 26 + return v >= 0 ? true : v === -2 ? null : v === -1 ? false : null 27 + } 28 + 29 + var streamNotify = Notify() 30 + layered.onEdge(function (j,k,v) { 31 + streamNotify({from:j, to:k, value:toLegacyValue(v)}) 32 + }) 33 + 34 + return { 35 + createFriendStream: function (opts) { 36 + var first = true 37 + return pull( 38 + layered.hopStream(opts), 39 + FlatMap(function (change) { 40 + var a = [] 41 + for(var k in change) 42 + if(!first || change[k] >= 0) 43 + a.push(opts && opts.meta ? {id: k, hops: change[k]} : k) 44 + first = false 45 + return a 46 + }) 47 + ) 48 + }, 49 + get: function (opts, cb) { 50 + if(!cb) 51 + cb = opts, opts = {} 52 + layered.onReady(function () { 53 + var value = layered.getGraph() 54 + //opts is used like this in ssb-ws 55 + if(opts && opts.source) { 56 + value = value[opts.source] 57 + if(value && opts.dest) 58 + cb(null, toLegacyValue(value[opts.dest])) 59 + else 60 + cb(null, map(value, toLegacyValue)) 61 + } 62 + else if( opts && opts.dest) { 63 + var _value = {} 64 + for(var k in value) 65 + if('undefined' !== typeof value[k][opts.dest]) 66 + _value[k] = value[k][opts.dest] 67 + cb(null, map(_value, toLegacyValue)) 68 + } 69 + else 70 + cb(null, mapGraph(value, toLegacyValue)) 71 + }) 72 + }, 73 + stream: function () { 74 + var source = streamNotify.listen() 75 + layered.onReady(function () { 76 + source.push(mapGraph(layered.getGraph(), toLegacyValue)) 77 + }) 78 + return source 79 + } 80 + } 81 + } 82 + 83 + function isEmpty (obj) { 84 + return typeof obj === 'object' && 85 + Object.keys(obj).length === 0 86 + } 87 + 88 + 89 + 90 + 91 +
+75
plugins/invite/README.md
··· 1 + # ssb-invite (classic, vendored) 2 + 3 + Invite-token system, mainly used for pubs. Creates invite codes as one of ways of onboarding. 4 + 5 + Generally this ends being used for pubs: 6 + 7 + - Users choose a pub from a [list of pubs](https://github.com/ssbc/ssb-server/wiki/Pub-Servers). 8 + - The chosen pub gives out an invite code to the user via the pub's website. 9 + - The user installs a Scuttlebutt client and copy and paste the invite code into the client's "accept invite" prompt. 10 + - The pub validates the invite code and follows back the new user, making them visible to other Scuttlebutt users. 11 + 12 + This project vendors the classic invite system used by secure-scuttlebot, and treats it as the canonical invite mechanism for this codebase. 13 + 14 + ## api 15 + 16 + ### create: async 17 + 18 + Create a new invite code. 19 + 20 + ```shell 21 + create {n} [{note}, {external}] 22 + ``` 23 + 24 + ```javascript 25 + create(n[, note, external], cb) 26 + ``` 27 + 28 + This produces an invite-code which encodes the ssb-server instance's public address, and a keypair seed. 29 + The keypair seed is used to generate a keypair, which is then used to authenticate a connection with the ssb-server instance. 30 + The ssb-server instance will then grant access to the `use` call. 31 + 32 + - `n` (number): How many times the invite can be used before it expires. 33 + - `note` (string): A note to associate with the invite code. The ssb-server instance will 34 + include this note in the follow message that it creates when `use` is 35 + called. 36 + - `external` (string): An external hostname to use 37 + 38 + 39 + ### accept: async 40 + 41 + Use an invite code. 42 + 43 + - invitecode (string) 44 + 45 + ```bash 46 + accept {invitecode} 47 + ``` 48 + 49 + ```js 50 + accept(invitecode, cb) 51 + ``` 52 + 53 + This connects to the server address encoded in the invite-code, then calls `use()` on the server. 54 + It will cause the server to follow the local user. 55 + 56 + 57 + ### use: async 58 + 59 + Use an invite code created by this ssb-server instance (advanced function). 60 + 61 + ```bash 62 + use --feed {feedid} 63 + ``` 64 + 65 + ```javascript 66 + use({ feed: }, cb) 67 + ``` 68 + 69 + This commands the receiving server to follow the given feed. 70 + 71 + An invite-code encodes the ssb-server instance's address, and a keypair seed. 72 + The keypair seed must be used to generate a keypair, then authenticate a connection with the ssb-server instance, in order to use this function. 73 + 74 + - `feed` (feedid): The feed the server should follow. 75 +
+38
plugins/invite/help.js
··· 1 + module.exports = { 2 + description: 'accept and create pub/followbot invites', 3 + commands: { 4 + create: { 5 + type: 'async', 6 + description: 'create an invite code, must be called on a pub with a static address', 7 + args: { 8 + uses: { 9 + type: 'number', 10 + description: 'number of times this invite may be used' 11 + }, 12 + modern: { 13 + type: 'boolean', 14 + description: 'return an invite which is also a multiserver address. all modern invites have a single use' 15 + }, 16 + external: { 17 + type: 'Host', 18 + description: "overide the pub's host name in the invite code" 19 + }, 20 + note: { 21 + type: 'any', 22 + description: 'metadata to attach to invite. this will be included in the contact message when the pub accepts this code' 23 + } 24 + } 25 + }, 26 + accept: { 27 + type: 'async', 28 + description: 'accept an invite, connects to the pub, requests invite, then follows pub if successful', 29 + args: { 30 + invite: { 31 + type: 'InviteCode', 32 + description: 'the invite code to accept' 33 + } 34 + } 35 + } 36 + } 37 + } 38 +
+288
plugins/invite/index.js
··· 1 + 'use strict' 2 + var valid = require('muxrpc-validation')({}) 3 + var crypto = require('crypto') 4 + var ssbKeys = require('ssb-keys') 5 + var cont = require('cont') 6 + var explain = require('explain-error') 7 + var ip = require('ip') 8 + var fs = require('fs') 9 + var ref = require('../ref') 10 + var level = require('level') 11 + var sublevel = require('level-sublevel/bytewise') 12 + var path = require('path') 13 + 14 + var createClient = require('ssb-client/client') 15 + 16 + // invite plugin 17 + // adds methods for producing invite-codes, 18 + // which peers can use to command your server to follow them. 19 + 20 + function isFunction (f) { 21 + return 'function' === typeof f 22 + } 23 + 24 + function isString (s) { 25 + return 'string' === typeof s 26 + } 27 + 28 + function isObject(o) { 29 + return o && 'object' === typeof o 30 + } 31 + 32 + function isNumber(n) { 33 + return 'number' === typeof n && !isNaN(n) 34 + } 35 + 36 + module.exports = { 37 + name: 'invite', 38 + version: '1.0.0', 39 + manifest: require('./manifest.json'), 40 + permissions: { 41 + master: {allow: ['create']}, 42 + //temp: {allow: ['use']} 43 + }, 44 + init: function (server, config) { 45 + var codes = {}, codesDB 46 + if(server.sublevel) 47 + codesDB = server.sublevel('codes') 48 + else { 49 + var db = sublevel(level(path.join(config.path, 'db'), { 50 + valueEncoding: 'json' 51 + })) 52 + codesDB = db.sublevel('codes') 53 + } 54 + //add an auth hook. 55 + server.auth.hook(function (fn, args) { 56 + var pubkey = args[0], cb = args[1] 57 + 58 + // run normal authentication 59 + fn(pubkey, function (err, auth) { 60 + if(err || auth) return cb(err, auth) 61 + 62 + // if no rights were already defined for this pubkey 63 + // check if the pubkey is one of our invite codes 64 + codesDB.get(pubkey, function (_, code) { 65 + //disallow if this invite has already been used. 66 + if(code && (code.used >= code.total)) cb() 67 + else cb(null, code && code.permissions) 68 + }) 69 + }) 70 + }) 71 + 72 + function getInviteAddress () { 73 + return (config.allowPrivate 74 + ? server.getAddress('public') || server.getAddress('local') || server.getAddress('private') 75 + : server.getAddress('public') 76 + ) 77 + } 78 + 79 + return { 80 + create: valid.async(function (opts, cb) { 81 + opts = opts || {} 82 + if(isNumber(opts)) 83 + opts = {uses: opts} 84 + else if(isObject(opts)) { 85 + if(opts.modern) 86 + opts.uses = 1 87 + } 88 + else if(isFunction(opts)) 89 + cb = opts, opts = {} 90 + 91 + var addr = getInviteAddress() 92 + if(!addr) return cb(new Error( 93 + 'no address available for creating an invite,'+ 94 + 'configuration needed for server.\n'+ 95 + 'see: https://github.com/ssbc/ssb-config/#connections' 96 + )) 97 + addr = addr.split(';').shift() 98 + var host = ref.parseAddress(addr).host 99 + if(typeof host !== 'string') { 100 + return cb(new Error('Could not parse host portion from server address:' + addr)) 101 + } 102 + 103 + if (opts.external) 104 + host = opts.external 105 + 106 + if(!config.allowPrivate && (ip.isPrivate(host) || 'localhost' === host || host === '')) 107 + return cb(new Error('Server has no public ip address, ' 108 + + 'cannot create useable invitation')) 109 + 110 + //this stuff is SECURITY CRITICAL 111 + //so it should be moved into the main app. 112 + //there should be something that restricts what 113 + //permissions the plugin can create also: 114 + //it should be able to diminish it's own permissions. 115 + 116 + // generate a key-seed and its key 117 + var seed = crypto.randomBytes(32) 118 + var keyCap = ssbKeys.generate('ed25519', seed) 119 + 120 + // store metadata under the generated pubkey 121 + var owner = server.id 122 + codesDB.put(keyCap.id, { 123 + id: keyCap.id, 124 + total: +opts.uses || 1, 125 + note: opts.note, 126 + used: 0, 127 + permissions: {allow: ['invite.use', 'getAddress'], deny: null} 128 + }, function (err) { 129 + // emit the invite code: our server address, plus the key-seed 130 + if(err) cb(err) 131 + else if(opts.modern) { 132 + var ws_addr = getInviteAddress().split(';').sort(function (a, b) { 133 + return +/^ws/.test(b) - +/^ws/.test(a) 134 + }).shift() 135 + 136 + 137 + if(!/^ws/.test(ws_addr)) throw new Error('not a ws address:'+ws_addr) 138 + cb(null, ws_addr+':'+seed.toString('base64')) 139 + } 140 + else { 141 + addr = ref.parseAddress(addr) 142 + cb(null, [opts.external ? opts.external : addr.host, addr.port, addr.key].join(':') + '~' + seed.toString('base64')) 143 + } 144 + }) 145 + }, 'number|object', 'string?'), 146 + use: valid.async(function (req, cb) { 147 + var rpc = this 148 + 149 + // fetch the code 150 + codesDB.get(rpc.id, function(err, invite) { 151 + if(err) return cb(err) 152 + 153 + // check if we're already following them 154 + server.friends.get(function (err, follows) { 155 + // server.friends.all('follow', function(err, follows) { 156 + // if(hops[req.feed] == 1) 157 + if (follows && follows[server.id] && follows[server.id][req.feed]) 158 + return cb(new Error('already following')) 159 + 160 + // although we already know the current feed 161 + // it's included so that request cannot be replayed. 162 + if(!req.feed) 163 + return cb(new Error('feed to follow is missing')) 164 + 165 + if(invite.used >= invite.total) 166 + return cb(new Error('invite has expired')) 167 + 168 + invite.used ++ 169 + 170 + //never allow this to be used again 171 + if(invite.used >= invite.total) { 172 + invite.permissions = {allow: [], deny: null} 173 + } 174 + //TODO 175 + //okay so there is a small race condition here 176 + //if people use a code massively in parallel 177 + //then it may not be counted correctly... 178 + //this is not a big enough deal to fix though. 179 + //-dominic 180 + 181 + // update code metadata 182 + codesDB.put(rpc.id, invite, function (err) { 183 + server.emit('log:info', ['invite', rpc.id, 'use', req]) 184 + 185 + // follow the user 186 + server.publish({ 187 + type: 'contact', 188 + contact: req.feed, 189 + following: true, 190 + pub: true, 191 + note: invite.note || undefined 192 + }, cb) 193 + }) 194 + }) 195 + }) 196 + }, 'object'), 197 + accept: valid.async(function (invite, cb) { 198 + // remove surrounding quotes, if found 199 + if(isObject(invite)) 200 + invite = invite.invite 201 + 202 + if (invite.charAt(0) === '"' && invite.charAt(invite.length - 1) === '"') 203 + invite = invite.slice(1, -1) 204 + var opts 205 + // connect to the address in the invite code 206 + // using a keypair generated from the key-seed in the invite code 207 + var modern = false 208 + if(ref.isInvite(invite)) { //legacy ivite 209 + if(ref.isLegacyInvite(invite)) { 210 + var parts = invite.split('~') 211 + opts = ref.parseAddress(parts[0])//.split(':') 212 + //convert legacy code to multiserver invite code. 213 + var protocol = 'net:' 214 + if (opts.host.endsWith(".onion")) 215 + protocol = 'onion:' 216 + invite = protocol+opts.host+':'+opts.port+'~shs:'+opts.key.slice(1, -8)+':'+parts[1] 217 + } 218 + else 219 + modern = true 220 + } 221 + 222 + opts = ref.parseAddress(ref.parseInvite(invite).remote) 223 + function connect (cb) { 224 + createClient({ 225 + keys: true, //use seed from invite instead. 226 + remote: invite, 227 + config: config, 228 + manifest: {invite: {use: 'async'}, getAddress: 'async'} 229 + }, cb) 230 + } 231 + 232 + // retry 3 times, with timeouts. 233 + // This is an UGLY hack to get the test/invite.js to pass 234 + // it's a race condition, I think because the server isn't ready 235 + // when it connects? 236 + 237 + function retry (fn, cb) { 238 + var n = 0 239 + ;(function next () { 240 + var start = Date.now() 241 + fn(function (err, value) { 242 + n++ 243 + if(n >= 3) cb(err, value) 244 + else if(err) setTimeout(next, 500 + (Date.now()-start)*n) 245 + else cb(null, value) 246 + }) 247 + })() 248 + } 249 + 250 + retry(connect, function (err, rpc) { 251 + 252 + if(err) return cb(explain(err, 'could not connect to server')) 253 + 254 + // command the peer to follow me 255 + rpc.invite.use({ feed: server.id }, function (err, msg) { 256 + if(err) return cb(explain(err, 'invite not accepted')) 257 + 258 + // follow and announce the pub 259 + cont.para([ 260 + cont(server.publish)({ 261 + type: 'contact', 262 + following: true, 263 + autofollow: true, 264 + contact: opts.key 265 + }), 266 + ( 267 + opts.host 268 + ? cont(server.publish)({ 269 + type: 'pub', 270 + address: opts 271 + }) 272 + : function (cb) { cb() } 273 + ) 274 + ]) 275 + (function (err, results) { 276 + if(err) return cb(err) 277 + rpc.close() 278 + rpc.close() 279 + //ignore err if this is new style invite 280 + if(server.gossip) server.gossip.add(ref.parseInvite(invite).remote, 'seed') 281 + cb(null, results) 282 + }) 283 + }) 284 + }) 285 + }, 'string') 286 + } 287 + } 288 + }
+6
plugins/invite/manifest.json
··· 1 + { 2 + "create": "async", 3 + "use": "async", 4 + "accept": "async" 5 + } 6 +
+324
plugins/ref/index.js
··· 1 + var isCanonicalBase64 = require('is-canonical-base64') 2 + var isDomain = require('is-valid-domain') 3 + var Querystring = require('querystring') 4 + var ip = require('ip') 5 + 6 + var parseLinkRegex = /^((@|%|&)[A-Za-z0-9\/+]{43}=\.[\w\d]+)(\?(.+))?$/ 7 + var linkRegex = exports.linkRegex = /^(@|%|&)[A-Za-z0-9\/+]{43}=\.[\w\d]+$/ 8 + var feedIdRegex = exports.feedIdRegex = isCanonicalBase64('@', '\.(?:sha256|ed25519)', 32) 9 + var blobIdRegex = exports.blobIdRegex = isCanonicalBase64('&', '\.sha256', 32) 10 + var msgIdRegex = exports.msgIdRegex = isCanonicalBase64('%', '\.sha256', 32) 11 + 12 + var extractRegex = /([@%&][A-Za-z0-9\/+]{43}=\.[\w\d]+)/ 13 + 14 + var MultiServerAddress = require('multiserver-address') 15 + 16 + function isMultiServerAddress (str) { 17 + //a http url fits into the multiserver scheme, 18 + //but all ssb address must have a transport and a transform 19 + //so check there is at least one unescaped ~ in the address 20 + return MultiServerAddress.check(str) && /[^!][~]/.test(str) 21 + } 22 + 23 + function isIP (s) { 24 + return ip.isV4Format(s) || ip.isV6Format(s) 25 + } 26 + 27 + var isInteger = Number.isInteger 28 + var DEFAULT_PORT = 8008 29 + 30 + function isString (s) { 31 + return 'string' === typeof s 32 + } 33 + 34 + var isHost = function (addr) { 35 + if (!isString(addr)) return 36 + addr = addr.replace(/^wss?:\/\//, '') 37 + return (isIP(addr)) || isDomain(addr) || addr === 'localhost' 38 + } 39 + 40 + var isPort = function (p) { 41 + return isInteger(p) && p <= 65536 42 + } 43 + 44 + function isObject (o) { 45 + return o && 'object' === typeof o && !Array.isArray(o) 46 + } 47 + 48 + var isFeedId = exports.isFeed = exports.isFeedId = 49 + function (data) { 50 + return isString(data) && feedIdRegex.test(data) 51 + } 52 + 53 + var isMsgId = exports.isMsg = exports.isMsgId = 54 + function (data) { 55 + return isString(data) && msgIdRegex.test(data) 56 + } 57 + 58 + var isBlobId = exports.isBlob = exports.isBlobId = 59 + function (data) { 60 + return isString(data) && blobIdRegex.test(data) 61 + } 62 + 63 + var isLink = exports.isLink = 64 + function (data) { 65 + if (!isString(data)) return false 66 + var index = data.indexOf('?') 67 + data = ~index ? data.substring(0, index) : data 68 + return isString(data) && (isFeedId(data) || isMsgId(data) || isBlobId(data)) 69 + } 70 + 71 + exports.isBlobLink = function (s) { 72 + return s[0] === '&' && isLink(s) 73 + } 74 + 75 + exports.isMsgLink = function (s) { 76 + return s[0] === '%' && isLink(s) 77 + } 78 + 79 + var normalizeChannel = exports.normalizeChannel = 80 + function (data) { 81 + if (typeof data === 'string') { 82 + data = data.toLowerCase().replace(/\s|,|\.|\?|!|<|>|\(|\)|\[|\]|"|#/g, '') 83 + if (data.length > 0 && data.length < 30) { 84 + return data 85 + } 86 + } 87 + } 88 + 89 + // Classic ssb-ref had deprecation wrappers around some APIs (console.trace). 90 + // For "classic" usage in this repo, we keep the APIs but drop the warnings. 91 + function deprecate (name, fn) { 92 + return fn 93 + } 94 + 95 + var parseMultiServerAddress = function (data) { 96 + if (!isString(data)) return false 97 + if (!MultiServerAddress.check(data)) return false 98 + 99 + var addr = MultiServerAddress.decode(data) 100 + addr = addr.find(function (address) { 101 + if (!address[0]) return false 102 + if (!address[1]) return false 103 + return /^(net|wss?|onion)$/.test(address[0].name) && /^shs/.test(address[1].name) 104 + }) 105 + if (!Array.isArray(addr)) { 106 + return false 107 + } 108 + var port = +addr[0].data.pop() //last item always port, to handle ipv6 109 + 110 + //preserve protocol type on websocket addresses 111 + var host = (/^wss?$/.test(addr[0].name) ? addr[0].name + ':' : '') + addr[0].data.join(':') 112 + var key = '@' + addr[1].data[0] + '.ed25519' 113 + var seed = addr[1].data[2] 114 + // allow multiserver addresses that are not currently understood! 115 + if (!(isHost(host) && isPort(+port) && isFeedId(key))) return false 116 + var address = { 117 + host: host, 118 + port: port, 119 + key: key 120 + } 121 + 122 + if (seed) address.seed = seed 123 + return address 124 + } 125 + 126 + var toLegacyAddress = parseMultiServerAddress 127 + exports.toLegacyAddress = deprecate('ssb-ref.toLegacyAddress', toLegacyAddress) 128 + 129 + var isLegacyAddress = exports.isLegacyAddress = function (addr) { 130 + return isObject(addr) && isHost(addr.host) && isPort(addr.port) && isFeedId(addr.key) 131 + } 132 + 133 + var toMultiServerAddress = exports.toMultiServerAddress = function (addr) { 134 + if (MultiServerAddress.check(addr)) return addr 135 + if (!isPort(addr.port)) throw new Error('ssb-ref.toMultiServerAddress - invalid port:' + addr.port) 136 + if (!isHost(addr.host)) throw new Error('ssb-ref.toMultiServerAddress - invalid host:' + addr.host) 137 + if (!isFeedId(addr.key)) throw new Error('ssb-ref.toMultiServerAddress - invalid key:' + addr.key) 138 + 139 + return ( 140 + /^wss?:/.test(addr.host) ? addr.host 141 + : /\.onion$/.test(addr.host) ? 'onion:' + addr.host 142 + : 'net:' + addr.host 143 + ) + ':' + addr.port + '~shs:' + addr.key.substring(1, addr.key.indexOf('.')) 144 + } 145 + 146 + var isAddress = exports.isAddress = function (data) { 147 + var host, port, id 148 + if (isObject(data)) { 149 + id = data.key; host = data.host; port = data.port 150 + } else if (!isString(data)) return false 151 + else if (isMultiServerAddress(data)) return true 152 + else { 153 + var parts = data.split(':') 154 + id = parts.pop(); port = parts.pop(); host = parts.join(':') 155 + } 156 + return ( 157 + isFeedId(id) && isPort(+port) && 158 + isHost(host) 159 + ) 160 + } 161 + 162 + //This is somewhat fragile, because maybe non-shs protocols get added... 163 + //it would be better to treat all addresses as opaque or have multiserver handle 164 + //extraction of a signing key from the address. 165 + var getKeyFromAddress = exports.getKeyFromAddress = function (addr) { 166 + if (addr.key) return addr.key 167 + var data = MultiServerAddress.decode(addr) 168 + if (!data) return 169 + for (var k in data) { 170 + var address = data[k] 171 + for (var j in address) { 172 + var protocol = address[j] 173 + if (/^shs/.test(protocol.name)) //forwards compatible with future shs versions... 174 + return '@' + protocol.data[0] + '.ed25519' 175 + } 176 + } 177 + } 178 + 179 + var parseAddress = function (e) { 180 + if (isString(e)) { 181 + if (~e.indexOf('~')) 182 + return parseMultiServerAddress(e) 183 + var parts = e.split(':') 184 + var id = parts.pop(), port = parts.pop(), host = parts.join(':') 185 + var e = { 186 + host: host, 187 + port: +(port || DEFAULT_PORT), 188 + key: id 189 + } 190 + return e 191 + } 192 + return e 193 + } 194 + exports.parseAddress = deprecate('ssb-ref.parseAddress', parseAddress) 195 + 196 + var toAddress = exports.toAddress = function (e) { 197 + e = parseAddress(e) 198 + e.port = e.port || DEFAULT_PORT 199 + e.host = e.host || 'localhost' 200 + return e 201 + } 202 + 203 + var legacyInviteRegex = /^[A-Za-z0-9\/+]{43}=$/ 204 + var legacyInviteFixerRegex = /#.*$/ 205 + var isLegacyInvite = exports.isLegacyInvite = 206 + function (data) { 207 + if (!isString(data)) return false 208 + data = data.replace(legacyInviteFixerRegex, '') 209 + var parts = data.split('~') 210 + return parts.length == 2 && isAddress(parts[0]) && legacyInviteRegex.test(parts[1]) 211 + } 212 + 213 + var isMultiServerInvite = exports.isMultiServerInvite = 214 + function (data) { 215 + if (!isString(data)) return false 216 + return !!parseMultiServerInvite(data) 217 + } 218 + 219 + var isInvite = exports.isInvite = 220 + function (data) { 221 + if (!isString(data)) return false 222 + return isLegacyInvite(data) || isMultiServerInvite(data) 223 + } 224 + 225 + exports.parseLink = function parseBlob (ref) { 226 + var match = parseLinkRegex.exec(ref) 227 + if (match && match[1]) { 228 + if (match[3]) { 229 + var query = Querystring.parse(match[4]) 230 + // unbox keys have a '+' in them that is parsed into a ' ', this changes it back 231 + if (isString(query.unbox)) query.unbox = query.unbox.replace(/ /g, '+') 232 + return { link: match[1], query: query } 233 + } else { 234 + return { link: match[1] } 235 + } 236 + } 237 + } 238 + 239 + function parseLegacyInvite (invite) { 240 + var redirect = invite.split('#') 241 + invite = redirect.shift() 242 + var parts = invite.split('~') 243 + var addr = toAddress(parts[0]) 244 + //convert legacy code to multiserver invite code. 245 + var remote = toMultiServerAddress(addr) 246 + invite = remote + ':' + parts[1] 247 + return { 248 + invite: remote + ':' + parts[1], 249 + key: addr.key, 250 + redirect: redirect.length ? '#' + redirect.join('#') : null 251 + } 252 + } 253 + 254 + function parseMultiServerInvite (invite) { 255 + var redirect = invite.split('#') 256 + if (!redirect.length) return null 257 + 258 + invite = redirect.shift() 259 + var addr = toLegacyAddress(invite) 260 + if (!addr) return null 261 + delete addr.seed 262 + return { 263 + invite: invite, 264 + remote: toMultiServerAddress(addr), 265 + key: addr.key, 266 + redirect: redirect.length ? '#' + redirect.join('#') : null 267 + } 268 + } 269 + 270 + exports.parseLegacyInvite = deprecate('ssb-ref.parseLegacyInvite', parseLegacyInvite) 271 + exports.parseMultiServerInvite = deprecate('ssb-ref.parseMultiServerInvite', parseMultiServerInvite) 272 + 273 + exports.parseInvite = deprecate('ssb-ref.parseInvite', function (invite) { 274 + return ( 275 + isLegacyInvite(invite) 276 + ? parseLegacyInvite(invite) 277 + : isMultiServerInvite(invite) 278 + ? parseMultiServerInvite(invite) 279 + : null 280 + ) 281 + }) 282 + 283 + exports.type = 284 + function (id) { 285 + if (!isString(id)) return false 286 + var c = id.charAt(0) 287 + if (c == '@' && isFeedId(id)) 288 + return 'feed' 289 + else if (c == '%' && isMsgId(id)) 290 + return 'msg' 291 + else if (c == '&' && isBlobId(id)) 292 + return 'blob' 293 + else if (isAddress(id)) return 'address' 294 + else if (isInvite(id)) return 'invite' 295 + else 296 + return false 297 + } 298 + 299 + exports.extract = 300 + function (data) { 301 + if (!isString(data)) 302 + return false 303 + 304 + var _data = data 305 + 306 + var res = extractRegex.exec(_data) 307 + if (res) { 308 + return res && res[0] 309 + } else { 310 + try { _data = decodeURIComponent(data) } 311 + catch (e) {} // this may fail if it's not encoded, so don't worry if it does 312 + _data = _data.replace(/&amp;/g, '&') 313 + 314 + var res = extractRegex.exec(_data) 315 + return res && res[0] 316 + } 317 + } 318 + 319 + 320 + 321 + 322 + 323 + 324 +
+1 -1
test/caps.js
··· 9 9 require('secret-stack')(require('./defaults')) 10 10 .use(require('ssb-db')) 11 11 .use(require('ssb-replicate')) 12 - .use(require('ssb-friends')) 12 + .use(require('../plugins/friends')) 13 13 .use(require('ssb-gossip')) 14 14 .use(require('ssb-logging')) 15 15
+1 -2
test/util.js
··· 1 - var ref = require('ssb-ref') 1 + var ref = require('../plugins/ref') 2 2 3 3 exports.follow = function (id) { 4 4 return { ··· 29 29 file: hash 30 30 } 31 31 } 32 -