Files: 18875b2a6630bee59ed985aafef99167cf8dfe00 / plugins / invite.js
7507 bytesRaw
1 | |
2 | var crypto = require('crypto') |
3 | var ssbKeys = require('ssb-keys') |
4 | var toAddress = require('../lib/util').toAddress |
5 | var cont = require('cont') |
6 | var explain = require('explain-error') |
7 | var ip = require('ip') |
8 | var mdm = require('mdmanifest') |
9 | var valid = require('../lib/validators') |
10 | var apidoc = require('../lib/apidocs').invite |
11 | var ref = require('ssb-ref') |
12 | |
13 | var 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 | |
19 | function isFunction (f) { |
20 | return 'function' === typeof f |
21 | } |
22 | |
23 | function isString (s) { |
24 | return 'string' === typeof s |
25 | } |
26 | |
27 | function isObject(o) { |
28 | return o && 'object' === typeof o |
29 | } |
30 | |
31 | function isNumber(n) { |
32 | return 'number' === typeof n && !isNaN(n) |
33 | } |
34 | |
35 | module.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