git ssb

4+

Dominic / scuttlebot



Tree: ff951035989067cd41a4127422e8cfb00361ebeb

Files: ff951035989067cd41a4127422e8cfb00361ebeb / plugins / invite.js

7507 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 var createClient = this.createClient
48
49 //add an auth hook.
50 server.auth.hook(function (fn, args) {
51 var pubkey = args[0], cb = args[1]
52
53 // run normal authentication
54 fn(pubkey, function (err, auth) {
55 if(err || auth) return cb(err, auth)
56
57 // if no rights were already defined for this pubkey
58 // check if the pubkey is one of our invite codes
59 codesDB.get(pubkey, function (_, code) {
60 //disallow if this invite has already been used.
61 if(code && (code.used >= code.total)) cb()
62 else cb(null, code && code.permissions)
63 })
64 })
65 })
66
67 return {
68 create: valid.async(function (opts, cb) {
69 opts = opts || {}
70 if(isNumber(opts))
71 opts = {uses: opts}
72 else if(isObject(opts)) {
73 if(opts.modern)
74 opts.uses = 1
75 }
76 else if(isFunction(opts))
77 cb = opts, opts = {}
78
79 var addr = server.getAddress()
80 var host = ref.parseAddress(addr).host
81 if(!config.allowPrivate && (ip.isPrivate(host) || 'localhost' === host))
82 return cb(new Error('Server has no public ip address, '
83 + 'cannot create useable invitation'))
84
85 //this stuff is SECURITY CRITICAL
86 //so it should be moved into the main app.
87 //there should be something that restricts what
88 //permissions the plugin can create also:
89 //it should be able to diminish it's own permissions.
90
91 // generate a key-seed and its key
92 var seed = crypto.randomBytes(32)
93 var keyCap = ssbKeys.generate('ed25519', seed)
94
95 // store metadata under the generated pubkey
96 var owner = server.id
97 codesDB.put(keyCap.id, {
98 id: keyCap.id,
99 total: +opts.uses || 1,
100 note: opts.note,
101 used: 0,
102 permissions: {allow: ['invite.use', 'getAddress'], deny: null}
103 }, function (err) {
104 // emit the invite code: our server address, plus the key-seed
105 if(err) cb(err)
106 else if(opts.modern && server.ws && server.ws.getAddress) {
107 cb(null, server.ws.getAddress()+':'+seed.toString('base64'))
108 }
109 else {
110 addr = ref.parseAddress(addr)
111 cb(null, [addr.host, addr.port, addr.key].join(':') + '~' + seed.toString('base64'))
112 }
113 })
114 }, 'number|object', 'string?'),
115 use: valid.async(function (req, cb) {
116 var rpc = this
117
118 // fetch the code
119 codesDB.get(rpc.id, function(err, invite) {
120 if(err) return cb(err)
121
122 // check if we're already following them
123 server.friends.get(function (err, follows) {
124// server.friends.all('follow', function(err, follows) {
125// if(hops[req.feed] == 1)
126 if (follows && follows[server.id] && follows[server.id][req.feed])
127 return cb(new Error('already following'))
128
129 // although we already know the current feed
130 // it's included so that request cannot be replayed.
131 if(!req.feed)
132 return cb(new Error('feed to follow is missing'))
133
134 if(invite.used >= invite.total)
135 return cb(new Error('invite has expired'))
136
137 invite.used ++
138
139 //never allow this to be used again
140 if(invite.used >= invite.total) {
141 invite.permissions = {allow: [], deny: null}
142 }
143 //TODO
144 //okay so there is a small race condition here
145 //if people use a code massively in parallel
146 //then it may not be counted correctly...
147 //this is not a big enough deal to fix though.
148 //-dominic
149
150 // update code metadata
151 codesDB.put(rpc.id, invite, function (err) {
152 server.emit('log:info', ['invite', rpc.id, 'use', req])
153
154 // follow the user
155 server.publish({
156 type: 'contact',
157 contact: req.feed,
158 following: true,
159 pub: true,
160 note: invite.note || undefined
161 }, cb)
162 })
163 })
164 })
165 }, 'object'),
166 accept: valid.async(function (invite, cb) {
167 // remove surrounding quotes, if found
168 if (invite.charAt(0) === '"' && invite.charAt(invite.length - 1) === '"')
169 invite = invite.slice(1, -1)
170 var opts
171 // connect to the address in the invite code
172 // using a keypair generated from the key-seed in the invite code
173 var modern = false
174 if(ref.isInvite(invite)) { //legacy ivite
175 if(ref.isLegacyInvite(invite)) {
176 var parts = invite.split('~')
177 opts = ref.parseAddress(parts[0])//.split(':')
178 //convert legacy code to multiserver invite code.
179 invite = 'net:'+opts.host+':'+opts.port+'~shs:'+opts.key.slice(1, -8)+':'+parts[1]
180 }
181 else
182 modern = true
183 }
184
185 opts = ref.parseAddress(ref.parseInvite(invite).remote)
186
187 ssbClient(null, {
188 remote: invite,
189 manifest: {invite: {use: 'async'}, getAddress: 'async'}
190 }, function (err, rpc) {
191 if(err) return cb(explain(err, 'could not connect to server'))
192
193 // command the peer to follow me
194 rpc.invite.use({ feed: server.id }, function (err, msg) {
195 if(err) return cb(explain(err, 'invite not accepted'))
196
197 // follow and announce the pub
198 cont.para([
199 server.publish({
200 type: 'contact',
201 following: true,
202 autofollow: true,
203 contact: opts.key
204 }),
205 (
206 opts.host
207 ? server.publish({
208 type: 'pub',
209 address: opts
210 })
211 : function (cb) { cb() }
212 )
213 ])
214 (function (err, results) {
215 if(err) return cb(err)
216 rpc.getAddress(function (err, addr) {
217 rpc.close()
218 //ignore err if this is new style invite
219 if(modern && err) return cb(err, addr)
220 if(server.gossip) server.gossip.add(addr, 'seed')
221 cb(null, results)
222 })
223 })
224 })
225 })
226 }, 'string')
227 }
228 }
229}
230
231

Built with git-ssb-web