git ssb

0+

Dominic / ssb-peer-invites



Tree: f2e147d8f75aa2f8c26ae7d14f089c68fe44e5cf

Files: f2e147d8f75aa2f8c26ae7d14f089c68fe44e5cf / index.js

15489 bytesRaw
1var pull = require('pull-stream')
2var Reduce = require('flumeview-reduce')
3var I = require('./valid')
4var deepEquals = require('deep-equals')
5var types = require('./types')
6var paramap = require('pull-paramap')
7var ssbClient = require('ssb-client')
8var crypto = require('crypto')
9var ssbKeys = require('ssb-keys')
10var u = require('./util')
11var cap = require('./cap')
12var explain = require('explain-error')
13
14function code(err, c) {
15 err.code = 'peer-invites:'+c
16 return err
17}
18
19function all (stream, cb) {
20 return pull(stream, pull.collect(cb))
21}
22
23function isFunction (f) {
24 return typeof f === 'function'
25}
26
27function isObject (o) {
28 return o && typeof o == 'object'
29}
30
31function isString (s) {
32 return typeof s == 'string'
33}
34
35function toBuffer(b) {
36 return Buffer.isBuffer(b) ? b : Buffer.from(b, 'base64')
37}
38
39exports.name = 'peer-invites'
40
41exports.version = '1.0.0'
42exports.manifest = {
43 getInvite: 'async',
44 confirm: 'async',
45 create: 'async',
46 willReplicate: 'async',
47 getNearbyPubs: 'async',
48 openInvite: 'async',
49 acceptInvite: 'async',
50 help: 'sync'
51}
52
53exports.permissions = {
54 anonymous: {allow: ['willReplicate']},
55}
56
57// KNOWN BUG: it's possible to accept an invite more than once,
58// but peers will ignore subsequent acceptances. it's possible
59// that this could create confusion in certain situations.
60// (but you'd get a feed that some peers thought was invited by Alice
61// other peers would think a different feed accepted that invite)
62// I guess the answer is to let alice reject the second invite?)
63// that would be easier to do if this was a levelreduce? (keys: reduce, instead of a single reduce?)
64
65exports.init = function (sbot, config) {
66 var init = false
67 var layer = sbot.friends.createLayer('peer-invites')
68
69 var caps = config.caps || {}
70 caps.peerInvite = caps.peerInvite || cap
71 var initial = {invites: {}, accepts: {}, hosts: {}, guests: {}}
72
73 function reduce (acc, data, _seq) {
74 if(!acc) acc = initial
75 var msg = data.value
76 var invite, accept
77 if(types.isInvite(msg, caps)) {
78 //TODO: validate that this is a msg we understand!
79 invite = msg
80 accept = acc.accepts[data.key]
81
82 //remember guest ids, so that you can process pub messages.
83 //this is necessary to confirm invites here the guest failed before they received
84 //the confirmation, and do not realize they are confirmed yet. they'll try again later.
85
86 //id rather not have this here. it's gonna bloat. better a different kind of index.
87 //can fix this later though.
88 acc.guests[data.value.content.invite] = true
89 }
90 else if(types.isAccept(msg, caps)) {
91 accept = msg
92 invite = acc.invites[accept.content.receipt]
93 }
94 else if(types.isConfirm(msg, caps)) {
95 //TODO: just for when we are the guest, but we need to make sure at least one confirm exists.
96 accept = msg.content.embed
97 invite = acc.invites[accept.content.receipt]
98 }
99
100 if(invite && accept) {
101 if(invite === true)
102 return acc
103 var invite_id = accept.content.receipt
104 try { I.verifyAccept(accept, invite, caps) }
105 catch (err) { return acc }
106 //fall through from not throwing
107
108 //delete matched invites, but _only_ if they are VALID. (returned in the catch if invalid)
109 delete acc.accepts[invite_id]
110 //but remember that this invite has been processed.
111 acc.invites[invite_id] = true
112 acc.hosts[invite.author] = acc.hosts[invite.author] || {}
113 acc.hosts[invite.author][accept.author] = 1
114 if(init) {
115 //interpret accepting an invite as a mutual follow.
116 layer(invite.author, accept.author, 1)
117 layer(accept.author, invite.author, 1)
118 }
119 }
120 else if(invite)
121 acc.invites[data.key] = invite
122 else if(accept)
123 acc.accepts[accept.receipt] = accept
124
125 return acc
126 }
127
128 var state
129 //a hack here, so that we can grab a handle on invites.value.set
130 var invites = sbot._flumeUse('peer-invites', function (log, name) {
131 var _invites = Reduce(3, reduce, null, null, initial)(log, name)
132 state = _invites.value
133 return _invites
134 })
135
136 invites.get(function (_, data) {
137 var g = {}
138 if(!data) layer({})
139 else {
140 //interpret accepted invites as two-way, but only store a minimal host->guest data structure
141 for(var j in data.hosts) {
142 for(var k in data.hosts[j]) {
143 g[j] = g[j] || {}
144 g[k] = g[k] || {}
145 g[j][k] = g[k][j] = 1
146 }
147 }
148 init = true
149 layer(g)
150 }
151 })
152
153 sbot.auth.hook(function (fn, args) {
154 var id = args[0], cb = args[1]
155 //currently a problem here where message may be confirmed,
156 //but guest didn't get the welcome yet. they need to be able to connect
157 //and request it again.
158 invites.get(function (err, v) {
159 if(err) return cb(err)
160 if(v.guests[id])
161 return cb(null, {
162 allow: ['peerInvites.getInvite', 'peerInvites.confirm'],
163 deny: null
164 })
165 fn.apply(null, args)
166 })
167 })
168
169 //retrive full invitation.
170 invites.getInvite = function (invite_id, cb) {
171 var self = this
172 invites.get(function (err, v) {
173 var invite = v.invites[invite_id]
174 if(err) return cb(err)
175 if(!invite)
176 cb(code(
177 new Error('unknown invite:'+invite_id),
178 'unknown-invite'
179 ))
180 else if(invite === true)
181 //TODO just retrive all confirmations we know about
182 //via links.
183 sbot.get(invite_id, cb)
184 //only allow the guest to request their own invite.
185 else if(self.id !== invite.content.invite)
186 cb(code(
187 new Error('invite did not match client id'),
188 'invite-mismatch'
189 ))
190 else
191 cb(null, v.invites[invite_id])
192 })
193 }
194
195 function getResponse (invite_id, test, cb) {
196 return all(
197 sbot.links({dest: invite_id, values: true, keys: false, meta: false}),
198 function (err, confirms) {
199 if(err) cb(err)
200 else cb(null,
201 confirms.filter(function (e) {
202 try {
203 return test(e)
204 } catch (err) {
205 return false
206 }
207 })[0]
208 )
209 }
210 )
211 }
212
213 var accepted = {}
214
215 function getConfirm (invite_id, accept, cb) {
216 getResponse(invite_id, function (msg) {
217 return (
218 msg.content.type === 'peer-invite/confirm' &&
219 msg.content.embed.content.receipt === invite_id &&
220 deepEquals(msg.content.embed, accept)
221 )
222 }, cb)
223 }
224
225 //used to request that a server confirms your acceptance.
226 invites.confirm = function (accept, cb) {
227 var invite_id = accept.content.receipt
228 //check if the invite in question hasn't already been accepted.
229 getConfirm(invite_id, accept, function (err, confirm) {
230 if(err) return cb(err)
231 if(confirm) return cb(null, confirm)
232
233 sbot.get(invite_id, function (err, invite) {
234 try {
235 I.verifyAccept(accept, invite, caps)
236 } catch (err) {
237 return cb(err)
238 }
239 //there is a little race condition here, if accept is called again
240 //before this write completes, it will write twice, so just return an error.
241 if(accepted[invite_id]) return cb(new Error('race condition: try again soon'))
242
243 accepted[invite_id] = true
244 sbot.publish(I.createConfirm(accept), function (err, data) {
245 delete accepted[invite_id]
246 cb(err, data.value)
247 })
248 })
249 })
250 }
251
252
253 //if the caller is someone we know, let them know wether
254 //we are willing to confirm (and replicate) their guest.
255 invites.willReplicate = function (opts, cb) {
256 if(isFunction(opts)) cb = opts, opts = {}
257 var id = this.id //id of caller
258 var max = config.friends && config.friends.hops || config.replicate && config.replicate.hops || 3
259 sbot.friends.hops({}, function (err, hops) {
260 // compare hops of caller (host to be) with max - 1
261 // because that means that the hops of the guest
262 // will be in range.
263 if(hops[id] <= (max - 1)) cb(null, true)
264 else cb(null, false)
265 })
266 }
267
268 function getAccept (invite_id, cb) {
269 getResponse(invite_id, function (msg) {
270 return (
271 msg.content.type === 'peer-invite/accept' &&
272 msg.content.receipt === invite_id
273 )
274 }, cb)
275 }
276
277 //retrive pubs who might be willing to confirm your invite. (used when creating an invte)
278 invites.getNearbyPubs = function (opts, cb) {
279 if(isFunction (opts))
280 cb = opts, opts = {}
281 var maxHops = opts.hops || 2
282 sbot.deviceAddress.getState(function (err, state) {
283 if(err) return cb(explain(err, 'could not retrive any device addresses'))
284 sbot.friends.hops({hops: opts.hops, reverse: true, start: opts.id}, function (err, hops) {
285 if(err) return cb(explain(err, 'could not retrive nearby friends'))
286 var near = []
287 for(var k in state) {
288 var da = state[k]
289 if(hops[k] <= maxHops) {
290 near.push({
291 id: k,
292 address: da.address,
293 hops: hops[k],
294 availability: da.availability
295 })
296 }
297 }
298 //sort by reverse hops, then by decending availability.
299 //default availibility
300 near.sort(function (a, b) {
301 return (
302 a.hops - b.hops ||
303 b.availability - a.availability
304 )
305 })
306
307 if(opts.offline) return cb(null, near)
308
309 var count = 3, found = []
310
311 function pushFound (pub, err, will) {
312 found.push({
313 id: pub.id, address: pub.address,
314 availability: pub.availability,
315 hops: pub.hops,
316 error: err && err.message, willReplicate: !!will
317 })
318 if(will) count --
319 //sort in order of wether they will replicate,
320 //or availability
321 found.sort(function (a, b) {
322 (!!b.willReplicate) - (!!a.willReplicate) || b.availability - a.availability
323 })
324 }
325
326 pull(
327 pull.values(near),
328 paramap(function (pub, cb) {
329 //if opts.id != sbot.id connect using ssb client
330 //so that you ask willReplicate from the correct id.
331 sbot.connect(pub.address, function (err, rpc) {
332 //skip pubs that were not contactable
333 if(err) {
334 pushFound(pub, err)
335 return cb()
336 }
337 rpc.peerInvites.willReplicate({}, function (err, v) {
338 //pass through input if true, else (err or false)
339 //then drop.
340 pushFound(pub, err, !!v)
341 cb(null, v && pub)
342 })
343 })
344 },3),
345 function (read) {
346 read(null, function next (err, pub) {
347 if(err) return cb(null, found)
348 else if(count) read(null, next)
349 else read(true, function (_) { cb(null, found) })
350 })
351 }
352 )
353 })
354 })
355 }
356
357 invites.create = function (opts, cb) {
358 if(isFunction(opts))
359 return opts(new Error ('peer-invites: expected: options *must* be provided.'))
360
361 var host_id = opts.id || sbot.id
362 invites.getNearbyPubs(opts, function (err, near) {
363 if(near.length == 0 && !opts.allowWithoutPubs)
364 return cb(new Error('failed to find any suitable pubs'))
365
366 var seed = crypto.randomBytes(32).toString('base64')
367 sbot.identities.publishAs({
368 id: host_id,
369 content: I.createInvite(seed, host_id, opts.reveal, opts.private, caps)
370 }, function (err, data) {
371 if(err) return cb(err)
372 var invite = {
373 seed: seed,
374 invite: data.key,
375 cap: opts.cap,
376 pubs: near.map(function (e) { return e.address }),
377 }
378 cb(null, u.stringify(invite))
379 })
380 })
381 }
382
383 //try each of an array of addresses, and cb the first one that works.
384 function connectFirst (invite, cb) {
385 var n = 0, err
386 var keys = ssbKeys.generate(null, toBuffer(invite.seed))
387 invite.pubs.forEach(function (addr) {
388 n++
389 //don't use sbot.connect here, because we are connecting
390 //with a different cap.
391 ssbClient(keys, {
392 remote: addr,
393 caps: {shs: invite.cap || caps.shs},
394 manifest: {
395 peerInvites: {
396 getInvite: 'async',
397 confirm: 'async'
398 }
399 }
400 }, function (_err, rpc) {
401 if(n > 0 && rpc) {
402 n = -1
403 cb(null, rpc)
404 } else {
405 err = err || _err
406 }
407 if(--n == 0) cb(explain(err, 'while trying to connect to:'+addr))
408 })
409 })
410 }
411
412 //TODO: check if invite is already held locally
413 // if so, just get it. when got, update local db.
414 invites.openInvite = function (invite, cb) {
415 if(isString(invite)) invite = u.parse(invite)
416 invites.getInvite(invite.invite, function (err, msg) {
417 if(msg)
418 next(msg)
419 else
420 connectFirst(invite, function (err, rpc) {
421 if(err) return cb(err)
422 rpc.peerInvites.getInvite(invite.invite, function (err, msg) {
423 if(err) return cb(err)
424 next(msg)
425 })
426 })
427
428 function next (msg) {
429 var invite_id = '%'+ssbKeys.hash(JSON.stringify(msg, null, 2))
430 if(invite.invite !== invite_id)
431 return cb(new Error(
432 'incorrect invite was returned! expected:'+invite.invite+', but got:'+invite_id
433 ))
434 var opened
435 try { opened = I.verifyInvitePrivate(msg, invite.seed, caps) }
436 catch (err) { return cb(err) }
437 //UPDATE REDUCE STATE.
438 // this is a wee bit naughty, because if you rebuild the index it might not have this invite
439 // (until you replicate it, but when you do the value won't change)
440 state.set(reduce(state.value, {key: invite_id, value:msg}, invites.since.value))
441 cb(null, msg, opened)
442 }
443 })
444 }
445
446 invites.acceptInvite = function (opts, cb) {
447 if(isString(opts)) opts = u.parse(opts)
448 var invite = isObject(opts.invite) ? opts.invite : opts
449 var invite_id = invite.invite
450 var id = opts.id || sbot.id
451
452 //check wether this invite is already accepted.
453 //or if the acceptance has been publish, but not yet confirmed.
454 getAccept(invite_id, function (err, accept) {
455 if(accept) next(accept)
456 else {
457 invites.openInvite(invite, function (err, invite_msg, opened) {
458 sbot.identities.publishAs({
459 id: id,
460 content: I.createAccept(invite_msg, invite.seed, id, caps)
461 }, function (err, accept) {
462 if(err) cb(err)
463 else {
464 state.set(reduce(state.value, accept, invites.since.value))
465 next(accept.value)
466 }
467 })
468 })
469 }
470 })
471
472 function next(accept) {
473 getConfirm(invite_id, accept, function (err, confirm) {
474 if(!confirm)
475 connectFirst(invite, function (err, rpc) {
476 if(err) return cb(err)
477 rpc.peerInvites.confirm(accept, function (err, confirm) {
478 //TODO: store confirms for us in the state.
479 cb(err, confirm)
480 })
481 })
482 })
483 }
484 }
485
486 invites.help = function () {
487 return require('./help')
488 }
489
490 return invites
491}
492
493// I am not happy with how big this file is
494// but I can't see a really good line along which to break it up.
495
496
497

Built with git-ssb-web