git ssb

0+

Dominic / ssb-peer-invites



Tree: 5f50cce8f8b985aae2b4894160b4ba9751ca8b0f

Files: 5f50cce8f8b985aae2b4894160b4ba9751ca8b0f / index.js

16008 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 = opts.min || 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 = u.sort(found)
322 }
323
324 pull(
325 pull.values(near),
326 paramap(function (pub, cb) {
327 //if opts.id != sbot.id connect using ssb client
328 //so that you ask willReplicate from the correct id.
329 sbot.connect(pub.address, function (err, rpc) {
330 //skip pubs that were not contactable
331 if(err) {
332 pushFound(pub, err)
333 return cb()
334 }
335 rpc.peerInvites.willReplicate({}, function (err, v) {
336 //pass through input if true, else (err or false)
337 //then drop.
338 pushFound(pub, err, !!v)
339 cb(null, v && pub)
340 })
341 })
342 },3),
343 function (read) {
344 read(null, function next (err, pub) {
345 if(err) return cb(null, found)
346 else if(count) read(null, next)
347 else read(true, function (_) { cb(null, found) })
348 })
349 }
350 )
351 })
352 })
353 }
354
355 invites.create = function (opts, cb) {
356 if(isFunction(opts))
357 return opts(new Error ('peer-invites: expected: options *must* be provided.'))
358
359 var host_id = opts.id || sbot.id
360 if(opts.allowWithoutPubs) {
361 var pubs = opts.pubs
362 pubs = Array.isArray(pubs) ? pubs : 'string' == typeof pubs ? pubs.split(',') : []
363 create(pubs)
364 }
365 else
366 invites.getNearbyPubs(opts, function (err, near) {
367 near = near.filter(function (e) {
368 return e.willReplicate
369 }).slice(0, opts.max || 3)
370 if(near.length == 0)
371 return cb(new Error('failed to find any suitable pubs'))
372 create(near.map(function (e) { return e.address }))
373 })
374
375 function create(pubs) {
376 var seed = crypto.randomBytes(32).toString('base64')
377 sbot.identities.publishAs({
378 id: host_id,
379 content: I.createInvite(seed, host_id, opts.reveal, opts.private, caps)
380 }, function (err, data) {
381 if(err) return cb(err)
382 var invite = {
383 seed: seed,
384 invite: data.key,
385 cap: opts.cap,
386 pubs: pubs,
387 }
388 cb(null, u.stringify(invite))
389 })
390 }
391 }
392
393 //try each of an array of addresses, and cb the first one that works.
394 function connectFirst (invite, cb) {
395 var n = 0, err
396 var keys = ssbKeys.generate(null, toBuffer(invite.seed))
397 var pubs = invite.pubs.filter(Boolean)
398 if(!pubs.length) return cb(new Error('peer-invites: invite missing pub addresses'))
399 pubs.forEach(function (addr) {
400 n++
401 //don't use sbot.connect here, because we are connecting
402 //with a different cap.
403 ssbClient(keys, {
404 remote: addr,
405 caps: {shs: invite.cap || caps.shs},
406 manifest: {
407 peerInvites: {
408 getInvite: 'async',
409 confirm: 'async'
410 }
411 }
412 }, function (_err, rpc) {
413 if(n > 0 && rpc) {
414 n = -1
415 cb(null, rpc)
416 } else {
417 err = err || _err
418 }
419 if(--n == 0) cb(explain(err, 'while trying to connect to:'+addr))
420 })
421 })
422 }
423
424 //TODO: check if invite is already held locally
425 // if so, just get it. when got, update local db.
426 invites.openInvite = function (invite, cb) {
427 if(isString(invite)) invite = u.parse(invite)
428 invites.getInvite(invite.invite, function (err, msg) {
429 if(msg)
430 next(msg)
431 else
432 connectFirst(invite, function (err, rpc) {
433 if(err) return cb(err)
434 rpc.peerInvites.getInvite(invite.invite, function (err, msg) {
435 if(err) return cb(err)
436 next(msg)
437 })
438 })
439
440 function next (msg) {
441 var invite_id = '%'+ssbKeys.hash(JSON.stringify(msg, null, 2))
442 if(invite.invite !== invite_id)
443 return cb(new Error(
444 'incorrect invite was returned! expected:'+invite.invite+', but got:'+invite_id
445 ))
446 var opened
447 try { opened = I.verifyInvitePrivate(msg, invite.seed, caps) }
448 catch (err) { return cb(err) }
449 //UPDATE REDUCE STATE.
450 // this is a wee bit naughty, because if you rebuild the index it might not have this invite
451 // (until you replicate it, but when you do the value won't change)
452 state.set(reduce(state.value, {key: invite_id, value:msg}, invites.since.value))
453 cb(null, {
454 key: invite_id,
455 value: msg,
456 opened: opened
457 })
458 }
459 })
460 }
461
462 invites.acceptInvite = function (opts, cb) {
463 if(isString(opts)) opts = u.parse(opts)
464 var invite = isObject(opts.invite) ? opts.invite : opts
465 var invite_id = invite.invite
466 var id = opts.id || sbot.id
467
468 //check wether this invite is already accepted.
469 //or if the acceptance has been publish, but not yet confirmed.
470 getAccept(invite_id, function (err, accept) {
471 if(accept) next(accept)
472 else {
473 invites.openInvite(invite, function (err, data) {
474 if(err) return cb(err)
475 var invite_msg = data.value
476 var opened = data.opened
477 sbot.identities.publishAs({
478 id: id,
479 content: I.createAccept(invite_msg, invite.seed, id, caps)
480 }, function (err, accept) {
481 if(err) cb(err)
482 else {
483 state.set(reduce(state.value, accept, invites.since.value))
484 next(accept.value)
485 }
486 })
487 })
488 }
489 })
490
491 function next(accept) {
492 getConfirm(invite_id, accept, function (err, confirm) {
493 if(!confirm)
494 connectFirst(invite, function (err, rpc) {
495 if(err) return cb(err)
496 rpc.peerInvites.confirm(accept, function (err, confirm) {
497 //TODO: store confirms for us in the state.
498 cb(err, confirm)
499 })
500 })
501 })
502 }
503 }
504
505 invites.help = function () {
506 return require('./help')
507 }
508
509 return invites
510}
511
512// I am not happy with how big this file is
513// but I can't see a really good line along which to break it up.
514

Built with git-ssb-web