git ssb

0+

Dominic / ssb-peer-invites



Tree: 391163e63d7f2d8f4b5c8568fb7bfe8c355a54d0

Files: 391163e63d7f2d8f4b5c8568fb7bfe8c355a54d0 / index.js

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

Built with git-ssb-web