git ssb

0+

Dominic / ssb-peer-invites



Tree: 0e94992aa8bedac43906183f616bff699ec2484b

Files: 0e94992aa8bedac43906183f616bff699ec2484b / index.js

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

Built with git-ssb-web