git ssb

1+

punkmonk.termux / mvd



forked from ev / mvd

Tree: d2b5693b01842e3376dae3c3cf5c579e03e490c4

Files: d2b5693b01842e3376dae3c3cf5c579e03e490c4 / plugins / invite.js

9099 bytesRaw
1'use strict'
2var crypto = require('crypto')
3var ssbKeys = require('ssb-keys')
4var toAddress = require('../lib/util').toAddress
5var cont = require('cont')
6var explain = require('explain-error')
7var ip = require('ip')
8var mdm = require('mdmanifest')
9var valid = require('../lib/validators')
10var apidoc = require('../lib/apidocs').invite
11var ref = require('ssb-ref')
12
13var ssbClient = require('ssb-client')
14
15// invite plugin
16// adds methods for producing invite-codes,
17// which peers can use to command your server to follow them.
18
19function isFunction (f) {
20 return 'function' === typeof f
21}
22
23function isString (s) {
24 return 'string' === typeof s
25}
26
27function isObject(o) {
28 return o && 'object' === typeof o
29}
30
31function isNumber(n) {
32 return 'number' === typeof n && !isNaN(n)
33}
34
35module.exports = {
36 name: 'invite',
37 version: '1.0.0',
38 manifest: mdm.manifest(apidoc),
39 permissions: {
40 master: {allow: ['create']},
41 //temp: {allow: ['use']}
42 },
43 init: function (server, config) {
44 var codes = {}
45 var codesDB = server.sublevel('codes')
46
47 //add an auth hook.
48 server.auth.hook(function (fn, args) {
49 var pubkey = args[0], cb = args[1]
50
51 // run normal authentication
52 fn(pubkey, function (err, auth) {
53 if(err || auth) return cb(err, auth)
54
55 // if no rights were already defined for this pubkey
56 // check if the pubkey is one of our invite codes
57 codesDB.get(pubkey, function (_, code) {
58 //disallow if this invite has already been used.
59 if(code && (code.used >= code.total)) cb()
60 else cb(null, code && code.permissions)
61 })
62 })
63 })
64
65 function getInviteAddress () {
66 return (config.allowPrivate
67 ? server.getAddress('public') || server.getAddress('local') || server.getAddress('private')
68 : server.getAddress('public')
69 )
70 }
71
72 return {
73 create: valid.async(function (opts, cb) {
74 opts = opts || {}
75 if(isNumber(opts))
76 opts = {uses: opts}
77 else if(isObject(opts)) {
78 if(opts.modern)
79 opts.uses = 1
80 }
81 else if(isFunction(opts))
82 cb = opts, opts = {}
83
84 var addr = getInviteAddress()
85 if(!addr) return cb(new Error(
86 'no address available for creating an invite,'+
87 'configuration needed for server.\n'+
88 'see: https://github.com/ssbc/ssb-config/#connections'
89 ))
90 addr = addr.split(';').shift()
91 var host = ref.parseAddress(addr).host
92 if(typeof host !== 'string') {
93 return cb(new Error('Could not parse host portion from server address:' + addr))
94 }
95
96 if (opts.external)
97 host = opts.external
98
99 if(!config.allowPrivate && (ip.isPrivate(host) || 'localhost' === host || host === ''))
100 return cb(new Error('Server has no public ip address, '
101 + 'cannot create useable invitation'))
102
103 //this stuff is SECURITY CRITICAL
104 //so it should be moved into the main app.
105 //there should be something that restricts what
106 //permissions the plugin can create also:
107 //it should be able to diminish it's own permissions.
108
109 // generate a key-seed and its key
110 var seed = crypto.randomBytes(32)
111 var keyCap = ssbKeys.generate('ed25519', seed)
112
113 // store metadata under the generated pubkey
114 var owner = server.id
115 codesDB.put(keyCap.id, {
116 id: keyCap.id,
117 total: +opts.uses || 1,
118 note: opts.note,
119 used: 0,
120 permissions: {allow: ['invite.use', 'getAddress'], deny: null}
121 }, function (err) {
122 // emit the invite code: our server address, plus the key-seed
123 if(err) cb(err)
124 else if(opts.modern) {
125 var ws_addr = getInviteAddress().split(';').sort(function (a, b) {
126 return +/^ws/.test(b) - +/^ws/.test(a)
127 }).shift()
128
129
130 if(!/^ws/.test(ws_addr)) throw new Error('not a ws address:'+ws_addr)
131 cb(null, ws_addr+':'+seed.toString('base64'))
132 }
133 else {
134 addr = ref.parseAddress(addr)
135 cb(null, [opts.external ? opts.external : addr.host, addr.port, addr.key].join(':') + '~' + seed.toString('base64'))
136 }
137 })
138 }, 'number|object', 'string?'),
139 use: valid.async(function (req, cb) {
140 var rpc = this
141
142 // fetch the code
143 codesDB.get(rpc.id, function(err, invite) {
144 if(err) return cb(err)
145
146 // check if we're already following them
147 server.friends.get(function (err, follows) {
148// server.friends.all('follow', function(err, follows) {
149// if(hops[req.feed] == 1)
150 if (follows && follows[server.id] && follows[server.id][req.feed])
151 return cb(new Error('already following'))
152
153 // although we already know the current feed
154 // it's included so that request cannot be replayed.
155 if(!req.feed)
156 return cb(new Error('feed to follow is missing'))
157
158 if(invite.used >= invite.total)
159 return cb(new Error('invite has expired'))
160
161 invite.used ++
162
163 //never allow this to be used again
164 if(invite.used >= invite.total) {
165 invite.permissions = {allow: [], deny: null}
166 }
167 //TODO
168 //okay so there is a small race condition here
169 //if people use a code massively in parallel
170 //then it may not be counted correctly...
171 //this is not a big enough deal to fix though.
172 //-dominic
173
174 // update code metadata
175 codesDB.put(rpc.id, invite, function (err) {
176 server.emit('log:info', ['invite', rpc.id, 'use', req])
177
178 // follow the user
179 server.publish({
180 type: 'contact',
181 contact: req.feed,
182 following: true,
183 pub: true,
184 note: invite.note || undefined
185 }, cb)
186 })
187 })
188 })
189 }, 'object'),
190 accept: valid.async(function (invite, cb) {
191 // remove surrounding quotes, if found
192 if (invite.charAt(0) === '"' && invite.charAt(invite.length - 1) === '"')
193 invite = invite.slice(1, -1)
194 var opts
195 // connect to the address in the invite code
196 // using a keypair generated from the key-seed in the invite code
197 var modern = false
198 if(ref.isInvite(invite)) { //legacy ivite
199 if(ref.isLegacyInvite(invite)) {
200 var parts = invite.split('~')
201 opts = ref.parseAddress(parts[0])//.split(':')
202 //convert legacy code to multiserver invite code.
203 var protocol = 'net:'
204 if (opts.host.endsWith(".onion"))
205 protocol = 'onion:'
206 invite = protocol+opts.host+':'+opts.port+'~shs:'+opts.key.slice(1, -8)+':'+parts[1]
207 }
208 else
209 modern = true
210 }
211
212 opts = ref.parseAddress(ref.parseInvite(invite).remote)
213 function connect (cb) {
214 ssbClient(null, {
215 caps: config.caps,
216 remote: invite,
217 manifest: {invite: {use: 'async'}, getAddress: 'async'}
218 }, cb)
219 }
220
221 // retry 3 times, with timeouts.
222 // This is an UGLY hack to get the test/invite.js to pass
223 // it's a race condition, I think because the server isn't ready
224 // when it connects?
225
226 function retry (fn, cb) {
227 var n = 0
228 ;(function next () {
229 var start = Date.now()
230 fn(function (err, value) {
231 n++
232 if(n >= 3) cb(err, value)
233 else if(err) setTimeout(next, 500 + (Date.now()-start)*n)
234 else cb(null, value)
235 })
236 })()
237 }
238
239 retry(connect, function (err, rpc) {
240
241 if(err) return cb(explain(err, 'could not connect to server'))
242
243 // command the peer to follow me
244 rpc.invite.use({ feed: server.id }, function (err, msg) {
245 if(err) return cb(explain(err, 'invite not accepted'))
246
247 // follow and announce the pub
248 cont.para([
249 server.publish({
250 type: 'contact',
251 following: true,
252 autofollow: true,
253 contact: opts.key
254 }),
255 (
256 opts.host
257 ? server.publish({
258 type: 'pub',
259 address: opts
260 })
261 : function (cb) { cb() }
262 )
263 ])
264 (function (err, results) {
265 if(err) return cb(err)
266 rpc.close()
267 rpc.close()
268 //ignore err if this is new style invite
269 if(server.gossip) server.gossip.add(ref.parseInvite(invite).remote, 'seed')
270 cb(null, results)
271 })
272 })
273 })
274 }, 'string')
275 }
276 }
277}
278
279

Built with git-ssb-web