UNPKG

7.68 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 async = require('async');
12var request = require('request');
13var VError = require('verror').VError;
14var WError = require('verror').WError;
15var _ = require('lodash');
16
17
18module.exports = function mountRequestFriend(antisocialApp) {
19
20 var router = antisocialApp.router;
21 var config = antisocialApp.config;
22 var db = antisocialApp.db;
23 var authUserMiddleware = antisocialApp.authUserMiddleware;
24
25 var testRegex = /^\/([a-zA-Z0-9\-.]+)\/request-friend$/;
26
27 debug('mounting GET /username/request-friend', testRegex);
28
29 router.get(testRegex, authUserMiddleware, function (req, res) {
30 var matches = req.path.match(testRegex);
31 var user = matches[1];
32
33 // must be a logged in user
34 var currentUser = req.antisocialUser;
35 if (!currentUser) {
36 debug('not logged in');
37 return res.sendStatus(401);
38 }
39
40 // by convention expects user to match username
41 if (currentUser.username !== user) {
42 debug('username mismatch');
43 return res.sendStatus(400);
44 }
45
46 var myEndPoint = config.publicHost + config.APIPrefix + '/' + currentUser.username;
47
48 // endpoint must be supplied and it must be a valid url
49 var endpoint = req.query.endpoint;
50 if (!endpoint) {
51 debug('endpoint not supplied');
52 return res.status(400).send('endpoint not supplied');
53 }
54
55 if (!endpoint.match(/(^|\s)((https?:\/\/)?[\w-]+(\.[\w-]+)+\.?(:\d+)?(\/\S*)?)/gi)) {
56 debug('endpoint not a valid url');
57 return res.status(400).send('endpoint not a valid url');
58 }
59
60 if (myEndPoint === req.query.endpoint) {
61 debug('can not friend yourself');
62 return res.status(400).send('can not friend yourself');
63 }
64
65 var remoteEndPoint = url.parse(req.query.endpoint);
66 var invite = req.query.invite;
67
68 async.waterfall([
69 function checkDupeFriend(cb) {
70 debug('/request-friend checkDupeFriend');
71 db.getInstances('friends', [{
72 'property': 'userId',
73 'value': currentUser.id
74 }, {
75 'property': 'remoteEndPoint',
76 'value': req.query.endpoint
77 }], function (err, friendInstances) {
78 if (err) {
79 return cb(new VError(err, 'error reading friends'));
80 }
81
82 if (friendInstances.length) {
83 return cb(new VError(err, 'duplicate friend request'));
84 }
85
86 cb();
87 });
88 },
89 function keyPair(cb) {
90 debug('/request-friend keyPair');
91 encryption.getKeyPair(function (err, pair) {
92 if (err) {
93 var e = new VError(err, '/request-friend keyPair failed');
94 return cb(e);
95 }
96 cb(null, pair);
97 });
98 },
99 function createPendingFriend(pair, cb) {
100 debug('/request-friend createPendingFriend');
101
102 var newFriend = {
103 'originator': true,
104 'status': 'pending',
105 'remoteEndPoint': req.query.endpoint,
106 'remoteHost': remoteEndPoint.protocol + '//' + remoteEndPoint.host,
107 'localRequestToken': uuid(),
108 'localAccessToken': uuid(),
109 'keys': pair,
110 'audiences': ['public'],
111 'hash': crc.crc32(req.query.endpoint).toString(16),
112 'userId': currentUser.id,
113 'inviteToken': invite,
114 'highWater': {}
115 };
116
117 db.newInstance('friends', newFriend, function (err, friendInstance) {
118 if (err) {
119 var e = new VError(err, '/request-friend createPendingFriend failed');
120 return cb(e);
121 }
122 cb(null, friendInstance);
123 });
124 },
125 function makeFriendRequest(friend, cb) {
126
127 var payload = {
128 'remoteEndPoint': myEndPoint,
129 'requestToken': friend.localRequestToken,
130 'inviteToken': invite
131 };
132
133 var options = {
134 'url': fixIfBehindProxy(friend.remoteEndPoint + '/friend-request'),
135 'form': payload,
136 'json': true,
137 'timeout': 10000
138 };
139
140 debug('/request-friend makeFriendRequest POST', options);
141
142 request.post(options, function (err, response, body) {
143 var e;
144
145 if (err) {
146 e = new VError(err, '/request-friend makeFriendRequest failed');
147 return cb(e, friend);
148 }
149
150 if (response.statusCode !== 200) {
151 e = new VError('/request-friend makeFriendRequest failed (http status: ' + response.statusCode + ' ' + body + ')');
152 return cb(e, friend);
153 }
154
155 if (_.get(body, 'status') !== 'ok') {
156 e = new VError('/request-friend makeFriendRequest failed (reason: ' + _.get(body, 'details') + ')');
157 return cb(e, friend);
158 }
159
160 debug('/request-friend makeFriendRequest got ', body);
161
162 cb(err, friend, body.requestToken);
163 });
164 },
165 function exchangeToken(friend, requestToken, cb) {
166
167 var payload = {
168 'endpoint': myEndPoint,
169 'requestToken': requestToken
170 };
171
172 var options = {
173 'url': fixIfBehindProxy(friend.remoteEndPoint + '/exchange-token'),
174 'form': payload,
175 'json': true
176 };
177
178 debug('/request-friend exchangeToken POST ', options);
179
180 request.post(options, function (err, response, body) {
181 if (err) {
182 var e = new VError(err, '/request-friend exchangeToken request error');
183 return cb(e, friend);
184 }
185 if (response.statusCode !== 200) {
186 var e = new VError(err, '/request-friend exchangeToken got http status: ' + response.statusCode);
187 return cb(e, friend);
188 }
189
190 if (!_.has(body, 'status') || !_.has(body, 'accessToken') || !_.has(body, 'publicKey')) {
191 e = new VError(err, '/request-friend exchangeToken got unexpected response %j', body);
192 return cb(e, friend);
193 }
194
195 debug('/request-friend exchangeToken got ', body);
196
197 cb(null, friend, body);
198 });
199 },
200 function saveToken(friend, exchange, cb) {
201
202 db.getInstances('friends', [{
203 'property': 'userId',
204 'value': currentUser.id
205 }], function (err, friends) {
206 if (err) {
207 var e = new VError(err, '/request-friend saveToken failed reading friends');
208 return cb(e, friend);
209 }
210
211 var unique = 0;
212 for (var i = 0; i < friends.length; i++) {
213 var friend = friends[i];
214 if (friend.remoteUsername === exchange.username) {
215 ++unique;
216 }
217 }
218
219 var update = {
220 'remoteAccessToken': exchange.accessToken,
221 'remotePublicKey': exchange.publicKey,
222 'remoteName': exchange.name,
223 'remoteUsername': exchange.username,
224 'uniqueRemoteUsername': unique ? exchange.username + '-' + unique : exchange.username
225 };
226
227 if (invite && exchange.status === 'accepted') {
228 update.status = 'accepted';
229 update.audiences = ['public', 'friends'];
230 }
231
232 db.updateInstance('friends', friend.id, update, function (err, friend) {
233 if (err) {
234 var e = new VError(err, '/request-friend saveToken error');
235 return cb(e, friend);
236 }
237
238 cb(null, friend);
239 });
240 });
241 }
242 ], function (err, friend) {
243 if (err) {
244
245 var e = new WError(err, 'request-friend failed');
246
247 res.send({
248 'status': 'error',
249 'reason': e.message,
250 'details': e.cause().message
251 });
252
253 if (friend) {
254 db.deleteInstance('friends', friend.id, function (err) {
255 if (err) {
256 console.log('/request-friend error deleting pending friend', err);
257 }
258 });
259 }
260 }
261 else {
262 if (friend.status === 'accepted') {
263 antisocialApp.emit('new-friend', currentUser, friend);
264 setTimeout(function () {
265 antisocialApp.activityFeed.connect(currentUser, friend);
266 }, 1000);
267 }
268
269 res.send({
270 'status': 'ok'
271 });
272 }
273 });
274 });
275};