UNPKG

8.5 kBJavaScriptView Raw
1// Copyright Michael Rhodes. 2017,2018. All Rights Reserved.
2// This file is licensed under the MIT License.
3// License text available at https://opensource.org/licenses/MIT
4
5var uuid = require('uuid');
6var crc = require('crc');
7var encryption = require('antisocial-encryption');
8var fixIfBehindProxy = require('../lib/utilities').fixIfBehindProxy;
9var url = require('url');
10var debug = require('debug')('antisocial-friends');
11var VError = require('verror').VError;
12var WError = require('verror').WError;
13var async = require('async');
14var request = require('request');
15var _ = require('lodash');
16
17module.exports = function mountFriendRequest(antisocialApp) {
18
19 var router = antisocialApp.router;
20 var config = antisocialApp.config;
21 var db = antisocialApp.db;
22 var authUserMiddleware = antisocialApp.authUserMiddleware;
23
24 var testRegex = /^\/([a-zA-Z0-9\-.]+)\/friend-request$/;
25
26 debug('mounting GET /username/friend-request', testRegex);
27
28 router.post(testRegex, function handleFriendRequest(req, res) {
29 var matches = req.path.match(testRegex);
30 var username = matches[1];
31
32 if (!req.body.remoteEndPoint) {
33 debug('remoteEndPoint not supplied');
34 return res.status(400).send('remoteEndPoint not supplied');
35 }
36
37 var requestToken = req.body.requestToken;
38 var invite = req.body.inviteToken;
39
40 if (!req.body.remoteEndPoint.match(/(^|\s)((https?:\/\/)?[\w-]+(\.[\w-]+)+\.?(:\d+)?(\/\S*)?)/gi)) {
41 debug('remoteEndPoint not a valid url');
42 return res.status(400).send('remoteEndPoint not a valid url');
43 }
44
45 var remoteEndPoint = url.parse(req.body.remoteEndPoint);
46
47 async.waterfall([
48 function getUser(cb) {
49 debug('/friend-request getUser');
50 db.getInstances('users', [{
51 'property': 'username',
52 'value': username
53 }], function (err, userInstances) {
54 if (err) {
55 return cb(new VError(err, 'user not found'));
56 }
57
58 if (userInstances.length > 1) {
59 return cb(new VError('more than one user matching username'));
60 }
61
62 if (!userInstances.length) {
63 return cb(new VError('user not found'));
64 }
65
66 var user = userInstances[0];
67
68 var myEndPoint = config.publicHost + config.APIPrefix + '/' + user.username;
69
70 if (myEndPoint === req.body.remoteEndPoint) {
71 return cb(new VError(err, 'can not friend yourself'));
72 }
73
74 cb(err, user);
75 });
76 },
77 function checkBlocked(user, cb) {
78 db.getInstances('blocks', [{
79 'property': 'userId',
80 'value': user.id
81 }, {
82 'property': 'remoteEndPoint',
83 'value': req.body.remoteEndPoint
84 }], function (err, blockedInstances) {
85 if (err) {
86 return cb(new VError(err, 'error reading blocks'));
87 }
88
89 if (blockedInstances.length) {
90 return cb(new VError(err, 'blocked'));
91 }
92
93 cb(null, user);
94 });
95 },
96 function checkDupeFriend(user, cb) {
97 debug('/friend-request checkDupeFriend');
98 db.getInstances('friends', [{
99 'property': 'userId',
100 'value': user.id
101 }, {
102 'property': 'remoteEndPoint',
103 'value': req.body.remoteEndPoint
104 }], function (err, friendInstances) {
105 if (err) {
106 return cb(new VError(err, 'error reading friends'));
107 }
108
109 if (friendInstances.length) {
110 return cb(new VError(err, 'duplicate friend request'));
111 }
112
113 cb(err, user);
114 });
115 },
116 function keyPair(user, cb) {
117 debug('/friend-request keyPair');
118 encryption.getKeyPair(function (err, pair) {
119 if (err) {
120 var e = new VError(err, '/friend-request keyPair failed');
121 return cb(e);
122 }
123 cb(null, user, pair);
124 });
125 },
126 function processInvite(user, pair, cb) {
127 if (!invite) {
128 return async.setImmediate(function () {
129 cb(null, user, pair, null);
130 });
131 }
132
133 db.getInstances('invitations', [{
134 'property': 'userId',
135 'value': user.id
136 }, {
137 'property': 'token',
138 'value': invite
139 }], function (err, invitations) {
140 if (err) {
141 var e = new VError(err, '/friend-request processInvite failed reading invitations');
142 return cb(e);
143 }
144
145 if (invitations.length > 1) {
146 return cb(new VError('/friend-request processInvite more than one user invitation token'));
147 }
148
149 if (!invitations.length) {
150 return cb(new VError('/friend-request processInvite invitation not found'));
151 }
152
153 cb(null, user, pair, invitations[0]);
154 });
155 },
156 function createPendingFriend(user, pair, invitation, cb) {
157 debug('/friend-request createPendingFriend');
158
159 var newFriend = {
160 'status': 'pending',
161 'remoteRequestToken': requestToken,
162 'remoteEndPoint': req.body.remoteEndPoint,
163 'remoteHost': remoteEndPoint.protocol + '//' + remoteEndPoint.host,
164 'localRequestToken': uuid(),
165 'localAccessToken': uuid(),
166 'keys': pair,
167 'audiences': ['public'],
168 'hash': crc.crc32(req.body.remoteEndPoint).toString(16),
169 'userId': user.id,
170 'inviteToken': invite,
171 'highWater': {}
172 };
173
174 db.newInstance('friends', newFriend, function (err, friendInstance) {
175 if (err) {
176 var e = new VError(err, '/friend-request createPendingFriend failed');
177 return cb(e);
178 }
179 cb(null, user, friendInstance, invitation);
180 });
181 },
182 function exchangeToken(user, friend, invitation, cb) {
183
184 var myEndPoint = config.publicHost + config.APIPrefix + '/' + user.username;
185 var payload = {
186 'endpoint': myEndPoint,
187 'requestToken': requestToken
188 };
189
190 var options = {
191 'url': fixIfBehindProxy(antisocialApp, friend.remoteEndPoint + '/exchange-token'),
192 'form': payload,
193 'json': true
194 };
195
196 debug('/friend-request exchangeToken POST ', options);
197
198 request.post(options, function (err, response, body) {
199 if (err) {
200 var e = new VError(err, '/friend-request exchangeToken request error');
201 return cb(e, user, friend);
202 }
203 if (response.statusCode !== 200) {
204 var e = new VError(err, '/friend-request exchangeToken got http status: ' + response.statusCode);
205 debug('/friend-request exchangeToken error %s %s', response.statusCode, body);
206 return cb(e, user, friend);
207 }
208
209 if (!_.has(body, 'status') || !_.has(body, 'accessToken') || !_.has(body, 'publicKey')) {
210 e = new VError(err, '/friend-request exchangeToken got unexpected response %j', body);
211 return cb(e, user, friend);
212 }
213
214 debug('/friend-request exchangeToken got ', body);
215
216 cb(null, user, friend, invitation, body);
217 });
218 },
219 function saveToken(user, friend, invitation, exchange, cb) {
220 db.getInstances('friends', [{
221 'property': 'userId',
222 'value': user.id
223 }], function (err, friends) {
224 if (err) {
225 var e = new VError(err, '/friend-request createPendingFriend failed reading friends');
226 return cb(e, user, friend);
227 }
228
229 var unique = 0;
230 for (var i = 0; i < friends.length; i++) {
231 var friend = friends[i];
232 if (friend.remoteUsername === exchange.username) {
233 ++unique;
234 }
235 }
236
237 var update = {
238 'remoteAccessToken': exchange.accessToken,
239 'remotePublicKey': exchange.publicKey,
240 'remoteName': exchange.name,
241 'remoteUsername': exchange.username,
242 'uniqueRemoteUsername': unique ? exchange.username + '-' + unique : exchange.username,
243 'community': exchange.community
244 };
245
246 if (invitation) {
247 update.status = 'accepted';
248 update.audiences = ['public', 'friends'];
249 }
250
251 db.updateInstance('friends', friend.id, update, function (err, friend) {
252 if (err) {
253 var e = new VError(err, '/friend-request saveToken error');
254 return cb(e, user, friend);
255 }
256
257 cb(null, user, friend);
258 });
259 });
260 }
261 ], function (err, user, friend) {
262 if (err) {
263 var e = new WError(err, 'request-friend failed');
264 debug('friend-request error', e.cause().message);
265 res.send({
266 'status': 'error',
267 'reason': e.message,
268 'details': e.cause().message
269 });
270
271 if (friend) {
272 db.deleteInstance('friends', friend.id, function (err) {
273 if (err) {
274 console.log('/friend-request error deleting pending friend', err);
275 }
276 });
277 }
278 }
279 else {
280 if (friend.inviteToken) {
281 antisocialApp.emit('new-friend', user, friend);
282 }
283 else {
284 antisocialApp.emit('new-friend-request', user, friend);
285 }
286
287 // if success hand a request token back to caller
288 res.send({
289 'status': 'ok',
290 'requestToken': friend.localRequestToken,
291 });
292 }
293 });
294 });
295};