git ssb

0+

Dominic / ssb-peer-invites



Tree: 7358227e3ee64f4328edbb8fa12bd7eb77fda214

Files: 7358227e3ee64f4328edbb8fa12bd7eb77fda214 / index.js

15411 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] = g[j] || {}
143 g[k] = g[k] || {}
144 g[j][k] = g[k][j] = 1
145 }
146 init = true
147 layer(g)
148 }
149 })
150
151 sbot.auth.hook(function (fn, args) {
152 var id = args[0], cb = args[1]
153 //currently a problem here where message may be confirmed,
154 //but guest didn't get the welcome yet. they need to be able to connect
155 //and request it again.
156 invites.get(function (err, v) {
157 if(err) return cb(err)
158 if(v.guests[id])
159 return cb(null, {
160 allow: ['peerInvites.getInvite', 'peerInvites.confirm'],
161 deny: null
162 })
163 fn.apply(null, args)
164 })
165 })
166
167 //retrive full invitation.
168 invites.getInvite = function (invite_id, cb) {
169 var self = this
170 invites.get(function (err, v) {
171 var invite = v.invites[invite_id]
172 if(err) return cb(err)
173 if(!invite)
174 cb(code(
175 new Error('unknown invite:'+invite_id),
176 'unknown-invite'
177 ))
178 else if(invite === true)
179 //TODO just retrive all confirmations we know about
180 //via links.
181 sbot.get(invite_id, cb)
182 //only allow the guest to request their own invite.
183 else if(self.id !== invite.content.invite)
184 cb(code(
185 new Error('invite did not match client id'),
186 'invite-mismatch'
187 ))
188 else
189 cb(null, v.invites[invite_id])
190 })
191 }
192
193 function getResponse (invite_id, test, cb) {
194 return all(
195 sbot.links({dest: invite_id, values: true, keys: false, meta: false}),
196 function (err, confirms) {
197 if(err) cb(err)
198 else cb(null,
199 confirms.filter(function (e) {
200 try {
201 return test(e)
202 } catch (err) {
203 return false
204 }
205 })[0]
206 )
207 }
208 )
209 }
210
211 var accepted = {}
212
213 function getConfirm (invite_id, accept, cb) {
214 getResponse(invite_id, function (msg) {
215 return (
216 msg.content.type === 'peer-invite/confirm' &&
217 msg.content.embed.content.receipt === invite_id &&
218 deepEquals(msg.content.embed, accept)
219 )
220 }, cb)
221 }
222
223 //used to request that a server confirms your acceptance.
224 invites.confirm = function (accept, cb) {
225 var invite_id = accept.content.receipt
226 //check if the invite in question hasn't already been accepted.
227 getConfirm(invite_id, accept, function (err, confirm) {
228 if(err) return cb(err)
229 if(confirm) return cb(null, confirm)
230
231 sbot.get(invite_id, function (err, invite) {
232 try {
233 I.verifyAccept(accept, invite, caps)
234 } catch (err) {
235 return cb(err)
236 }
237 //there is a little race condition here, if accept is called again
238 //before this write completes, it will write twice, so just return an error.
239 if(accepted[invite_id]) return cb(new Error('race condition: try again soon'))
240
241 accepted[invite_id] = true
242 sbot.publish(I.createConfirm(accept), function (err, data) {
243 delete accepted[invite_id]
244 cb(err, data.value)
245 })
246 })
247 })
248 }
249
250
251 //if the caller is someone we know, let them know wether
252 //we are willing to confirm (and replicate) their guest.
253 invites.willReplicate = function (opts, cb) {
254 if(isFunction(opts)) cb = opts, opts = {}
255 var id = this.id //id of caller
256 var max = config.friends && config.friends.hops || config.replicate && config.replicate.hops || 3
257 sbot.friends.hops({}, function (err, hops) {
258 // compare hops of caller (host to be) with max - 1
259 // because that means that the hops of the guest
260 // will be in range.
261 if(hops[id] <= (max - 1)) cb(null, true)
262 else cb(null, false)
263 })
264 }
265
266 function getAccept (invite_id, cb) {
267 getResponse(invite_id, function (msg) {
268 return (
269 msg.content.type === 'peer-invite/accept' &&
270 msg.content.receipt === invite_id
271 )
272 }, cb)
273 }
274
275 //retrive pubs who might be willing to confirm your invite. (used when creating an invte)
276 invites.getNearbyPubs = function (opts, cb) {
277 if(isFunction (opts))
278 cb = opts, opts = {}
279 var maxHops = opts.hops || 2
280 sbot.deviceAddress.getState(function (err, state) {
281 if(err) return cb(explain(err, 'could not retrive any device addresses'))
282 sbot.friends.hops({hops: opts.hops, reverse: true, start: opts.id}, function (err, hops) {
283 if(err) return cb(explain(err, 'could not retrive nearby friends'))
284 var near = []
285 for(var k in state) {
286 var da = state[k]
287 if(hops[k] <= maxHops) {
288 near.push({
289 id: k,
290 address: da.address,
291 hops: hops[k],
292 availability: da.availability
293 })
294 }
295 }
296 //sort by reverse hops, then by decending availability.
297 //default availibility
298 near.sort(function (a, b) {
299 return (
300 a.hops - b.hops ||
301 b.availability - a.availability
302 )
303 })
304
305 if(opts.offline) return cb(null, near)
306
307 var count = 3, found = []
308
309 function pushFound (pub, err, will) {
310 found.push({
311 id: pub.id, address: pub.address,
312 availability: pub.availability,
313 hops: pub.hops,
314 error: err && err.message, willReplicate: !!will
315 })
316 if(will) count --
317 //sort in order of wether they will replicate,
318 //or availability
319 found.sort(function (a, b) {
320 (!!b.willReplicate) - (!!a.willReplicate) || b.availability - a.availability
321 })
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 invites.getNearbyPubs(opts, function (err, near) {
361 if(near.length == 0 && !opts.allowWithoutPubs)
362 return cb(new Error('failed to find any suitable pubs'))
363
364 var seed = crypto.randomBytes(32).toString('base64')
365 sbot.identities.publishAs({
366 id: host_id,
367 content: I.createInvite(seed, host_id, opts.reveal, opts.private, caps)
368 }, function (err, data) {
369 if(err) return cb(err)
370 var invite = {
371 seed: seed,
372 invite: data.key,
373 cap: opts.cap,
374 pubs: near.map(function (e) { return e.address }),
375 }
376 cb(null, u.stringify(invite))
377 })
378 })
379 }
380
381 //try each of an array of addresses, and cb the first one that works.
382 function connectFirst (invite, cb) {
383 var n = 0, err
384 var keys = ssbKeys.generate(null, toBuffer(invite.seed))
385 invite.pubs.forEach(function (addr) {
386 n++
387 //don't use sbot.connect here, because we are connecting
388 //with a different cap.
389 ssbClient(keys, {
390 remote: addr,
391 caps: {shs: invite.cap || caps.shs},
392 manifest: {
393 peerInvites: {
394 getInvite: 'async',
395 confirm: 'async'
396 }
397 }
398 }, function (_err, rpc) {
399 if(n > 0 && rpc) {
400 n = -1
401 cb(null, rpc)
402 } else {
403 err = err || _err
404 }
405 if(--n == 0) cb(explain(err, 'while trying to connect to:'+remote))
406 })
407 })
408 }
409
410 //TODO: check if invite is already held locally
411 // if so, just get it. when got, update local db.
412 invites.openInvite = function (invite, cb) {
413 if(isString(invite)) invite = u.parse(invite)
414 invites.getInvite(invite.invite, function (err, msg) {
415 if(msg)
416 next(msg)
417 else
418 connectFirst(invite, function (err, rpc) {
419 if(err) return cb(err)
420 rpc.peerInvites.getInvite(invite.invite, function (err, msg) {
421 if(err) return cb(err)
422 next(msg)
423 })
424 })
425
426 function next (msg) {
427 var invite_id = '%'+ssbKeys.hash(JSON.stringify(msg, null, 2))
428 if(invite.invite !== invite_id)
429 return cb(new Error(
430 'incorrect invite was returned! expected:'+invite.invite+', but got:'+inviteId
431 ))
432 var opened
433 try { opened = I.verifyInvitePrivate(msg, invite.seed, caps) }
434 catch (err) { return cb(err) }
435 //UPDATE REDUCE STATE.
436 // this is a wee bit naughty, because if you rebuild the index it might not have this invite
437 // (until you replicate it, but when you do the value won't change)
438 state.set(reduce(state.value, {key: invite_id, value:msg}, invites.since.value))
439 cb(null, msg, opened)
440 }
441 })
442 }
443
444 invites.acceptInvite = function (opts, cb) {
445 if(isString(opts)) opts = u.parse(opts)
446 var invite = isObject(opts.invite) ? opts.invite : opts
447 var invite_id = invite.invite
448 var id = opts.id || sbot.id
449
450 //check wether this invite is already accepted.
451 //or if the acceptance has been publish, but not yet confirmed.
452 getAccept(invite_id, function (err, accept) {
453 if(accept) next(accept)
454 else {
455 invites.openInvite(invite, function (err, invite_msg, opened) {
456 sbot.identities.publishAs({
457 id: id,
458 content: I.createAccept(invite_msg, invite.seed, id, caps)
459 }, function (err, accept) {
460 if(err) cb(err)
461 else {
462 state.set(reduce(state.value, accept, invites.since.value))
463 next(accept.value)
464 }
465 })
466 })
467 }
468 })
469
470 function next(accept) {
471 getConfirm(invite_id, accept, function (err, confirm) {
472 if(!confirm)
473 connectFirst(invite, function (err, rpc) {
474 if(err) return cb(err)
475 rpc.peerInvites.confirm(accept, function (err, confirm) {
476 //TODO: store confirms for us in the state.
477 cb(err, confirm)
478 })
479 })
480 })
481 }
482 }
483
484 return invites
485}
486
487// I am not happy with how big this file is
488// but I can't see a really good line along which to break it up.
489
490
491

Built with git-ssb-web