Commit 1761e4e2 authored by Andrew Newdigate's avatar Andrew Newdigate

Feature: track users using fingerprinting technology

parent d8707309
'use strict';
var Fingerprint = require('gitter-web-persistence').Fingerprint;
var mongooseUtils = require('gitter-web-persistence-utils/lib/mongoose-utils');
var ObjectID = require('mongodb').ObjectID;
var MAX_FINGERPRINTS_PER_USER = 5;
function recordFingerprint(userId, fingerprint, ipAddress) {
return mongooseUtils.safeUpsertUpdate(Fingerprint, { userId: userId }, {
$setOnInsert: {
userId: userId
},
$push: {
fingerprints: {
$each: [{
_id: new ObjectID(),
fingerprint: fingerprint,
ipAddress: ipAddress
}],
$sort: {
_id: -1 // Keep the most recent ones
},
$slice: MAX_FINGERPRINTS_PER_USER
}
}
});
}
module.exports = {
recordFingerprint: recordFingerprint
};
{
"name": "gitter-web-fingerprinting",
"version": "1.0.0",
"main": "lib/index.js",
"directories": {
"lib": "lib",
"test": "test"
},
"scripts": {
"test": "mocha test/"
},
"dependencies": {
"bluebird": "^3.2.1",
"debug": "^2.2.0",
"gitter-web-env": "file:../env",
"gitter-web-persistence": "file:../persistence",
"gitter-web-persistence-utils": "file:../persistence-utils",
"mongodb": "~2.1.18"
},
"private": true
}
{
"env": {
"commonjs": true,
"node": true,
"mocha": true
},
"plugins": [
"mocha"
],
"rules": {
"node/no-unpublished-require": "off"
}
}
'use strict';
var fingerprintingService = require('../lib/fingerprinting-service');
var Fingerprint = require('gitter-web-persistence').Fingerprint;
var assert = require('assert');
var ObjectID = require('mongodb').ObjectID;
describe('fingerprinting-service', function() {
describe('integration tests #slow', function() {
it('should fingerprint', function() {
var userId = new ObjectID();
return fingerprintingService.recordFingerprint(userId, 'test', '192.168.0.1')
.then(function() {
return Fingerprint.findOne({ userId: userId }, { }, { lean: true })
})
.then(function(record) {
assert.strictEqual(String(record.userId), String(userId));
assert.strictEqual(record.fingerprints.length, 1);
assert(record.fingerprints[0]._id);
assert.strictEqual(record.fingerprints[0].fingerprint, 'test');
assert.strictEqual(record.fingerprints[0].ipAddress, '192.168.0.1');
})
})
it('should keep the 5 most recent fingerprints', function() {
var userId = new ObjectID();
return fingerprintingService.recordFingerprint(userId, 'test1', '192.168.0.1')
.then(function() {
return fingerprintingService.recordFingerprint(userId, 'test2', '192.168.0.2')
})
.then(function() {
return fingerprintingService.recordFingerprint(userId, 'test3', '192.168.0.3')
})
.then(function() {
return fingerprintingService.recordFingerprint(userId, 'test4', '192.168.0.4')
})
.then(function() {
return fingerprintingService.recordFingerprint(userId, 'test5', '192.168.0.5')
})
.then(function() {
return fingerprintingService.recordFingerprint(userId, 'test6', '192.168.0.6')
})
.then(function() {
return Fingerprint.findOne({ userId: userId }, { }, { lean: true })
})
.then(function(record) {
assert.strictEqual(String(record.userId), String(userId));
assert.strictEqual(record.fingerprints.length, 5);
assert.deepEqual(record.fingerprints.map(function(f) {
return f.fingerprint;
}), ['test6', 'test5', 'test4', 'test3', 'test2']);
assert.deepEqual(record.fingerprints.map(function(f) {
return f.ipAddress;
}), ['192.168.0.6', '192.168.0.5', '192.168.0.4', '192.168.0.3', '192.168.0.2']);
})
})
});
});
......@@ -86,6 +86,7 @@ var schemas = {
TroupeRemovedUser: require('./schemas/troupe-removed-user-schema'),
TroupeInvite: require('./schemas/troupe-invite-schema'),
KnownExternalAccess: require('./schemas/known-external-access-schema'),
Fingerprint: require('./schemas/fingerprint-schema'),
/* Topics */
Forum: require('./schemas/forum-schema'),
......
'use strict';
var mongoose = require('gitter-web-mongoose-bluebird');
var Schema = mongoose.Schema;
var FingerprintSchema = new Schema({
userId: { type: String, required: true },
fingerprints: [{
fingerprint: { type: String, required: true },
ipAddress: { type: String, required: false }
}],
}, { strict: 'throw' });
FingerprintSchema.schemaTypeName = 'FingerprintSchema';
FingerprintSchema.index({ userId: 1 }, { unique: true, background: true });
FingerprintSchema.index({ 'fingerprints.fingerprint': 1 }, { background: true });
FingerprintSchema.index({ 'fingerprints.ipAddress': 1 }, { background: true });
module.exports = {
install: function(mongooseConnection) {
var Model = mongooseConnection.model('Fingerprint', FingerprintSchema);
return {
model: Model,
schema: FingerprintSchema
};
}
};
......@@ -390,7 +390,7 @@
"dependencies": {
"redis": {
"version": "2.6.2",
"from": "redis@>=2.1.0 <3.0.0",
"from": "redis@latest",
"resolved": "http://beta-internal:4873/redis/-/redis-2.6.2.tgz",
"dependencies": {
"double-ended-queue": {
......@@ -666,9 +666,9 @@
"resolved": "http://beta-internal:4873/promise/-/promise-7.1.1.tgz",
"dependencies": {
"asap": {
"version": "2.0.4",
"version": "2.0.5",
"from": "asap@>=2.0.3 <2.1.0",
"resolved": "http://beta-internal:4873/asap/-/asap-2.0.4.tgz"
"resolved": "http://beta-internal:4873/asap/-/asap-2.0.5.tgz"
}
}
}
......@@ -1486,9 +1486,9 @@
"resolved": "http://beta-internal:4873/async/-/async-2.0.1.tgz",
"dependencies": {
"lodash": {
"version": "4.16.1",
"version": "4.16.2",
"from": "lodash@>=4.8.0 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.1.tgz"
"resolved": "http://beta-internal:4873/lodash/-/lodash-4.16.2.tgz"
}
}
}
......@@ -1782,9 +1782,9 @@
}
},
"lodash": {
"version": "4.16.1",
"version": "4.16.2",
"from": "lodash@>=4.6.1 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.1.tgz"
"resolved": "http://beta-internal:4873/lodash/-/lodash-4.16.2.tgz"
},
"mandrill-api": {
"version": "1.0.45",
......@@ -2007,7 +2007,7 @@
"dependencies": {
"redis": {
"version": "2.6.2",
"from": "redis@>=2.1.0 <3.0.0",
"from": "redis@latest",
"resolved": "http://beta-internal:4873/redis/-/redis-2.6.2.tgz",
"dependencies": {
"double-ended-queue": {
......@@ -2337,12 +2337,17 @@
"resolved": "file:modules/fake-data",
"dependencies": {
"lodash": {
"version": "4.16.1",
"version": "4.16.2",
"from": "lodash@>=4.13.1 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.1.tgz"
"resolved": "http://beta-internal:4873/lodash/-/lodash-4.16.2.tgz"
}
}
},
"gitter-web-fingerprinting": {
"version": "1.0.0",
"from": "modules/fingerprinting",
"resolved": "file:modules/fingerprinting"
},
"gitter-web-forums": {
"version": "1.0.0",
"from": "modules/forums",
......@@ -2489,9 +2494,9 @@
"resolved": "http://beta-internal:4873/backbone/-/backbone-1.2.3.tgz"
},
"lodash": {
"version": "4.16.1",
"version": "4.16.2",
"from": "lodash@>=4.15.0 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.1.tgz"
"resolved": "http://beta-internal:4873/lodash/-/lodash-4.16.2.tgz"
},
"sinon": {
"version": "1.17.6",
......@@ -2834,9 +2839,9 @@
"resolved": "http://beta-internal:4873/async/-/async-2.0.1.tgz",
"dependencies": {
"lodash": {
"version": "4.16.1",
"version": "4.16.2",
"from": "lodash@>=4.8.0 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.1.tgz"
"resolved": "http://beta-internal:4873/lodash/-/lodash-4.16.2.tgz"
}
}
}
......@@ -3697,7 +3702,7 @@
"mquery": {
"version": "1.11.0",
"from": "mquery@1.11.0",
"resolved": "https://registry.npmjs.org/mquery/-/mquery-1.11.0.tgz",
"resolved": "http://beta-internal:4873/mquery/-/mquery-1.11.0.tgz",
"dependencies": {
"bluebird": {
"version": "2.10.2",
......@@ -4304,9 +4309,9 @@
"resolved": "http://beta-internal:4873/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
"dependencies": {
"node-fetch": {
"version": "1.6.1",
"version": "1.6.3",
"from": "node-fetch@>=1.0.1 <2.0.0",
"resolved": "http://beta-internal:4873/node-fetch/-/node-fetch-1.6.1.tgz",
"resolved": "http://beta-internal:4873/node-fetch/-/node-fetch-1.6.3.tgz",
"dependencies": {
"encoding": {
"version": "0.1.12",
......@@ -4340,9 +4345,9 @@
"resolved": "http://beta-internal:4873/promise/-/promise-7.1.1.tgz",
"dependencies": {
"asap": {
"version": "2.0.4",
"version": "2.0.5",
"from": "asap@>=2.0.3 <2.1.0",
"resolved": "http://beta-internal:4873/asap/-/asap-2.0.4.tgz"
"resolved": "http://beta-internal:4873/asap/-/asap-2.0.5.tgz"
}
}
},
......@@ -4617,7 +4622,7 @@
"dependencies": {
"chalk": {
"version": "1.1.3",
"from": "chalk@>=1.1.1 <2.0.0",
"from": "chalk@>=1.1.3 <2.0.0",
"resolved": "http://beta-internal:4873/chalk/-/chalk-1.1.3.tgz",
"dependencies": {
"ansi-styles": {
......@@ -5679,9 +5684,9 @@
}
},
"cross-spawn": {
"version": "4.0.0",
"version": "4.0.2",
"from": "cross-spawn@>=4.0.0 <5.0.0",
"resolved": "http://beta-internal:4873/cross-spawn/-/cross-spawn-4.0.0.tgz",
"resolved": "http://beta-internal:4873/cross-spawn/-/cross-spawn-4.0.2.tgz",
"dependencies": {
"lru-cache": {
"version": "4.0.1",
......@@ -6093,9 +6098,9 @@
"resolved": "http://beta-internal:4873/async/-/async-2.0.1.tgz",
"dependencies": {
"lodash": {
"version": "4.16.1",
"version": "4.16.2",
"from": "lodash@>=4.8.0 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.1.tgz"
"resolved": "http://beta-internal:4873/lodash/-/lodash-4.16.2.tgz"
}
}
}
......@@ -6108,7 +6113,7 @@
"dependencies": {
"chalk": {
"version": "1.1.3",
"from": "chalk@>=1.0.0 <2.0.0",
"from": "chalk@>=1.1.1 <2.0.0",
"resolved": "http://beta-internal:4873/chalk/-/chalk-1.1.3.tgz",
"dependencies": {
"ansi-styles": {
......@@ -6449,7 +6454,7 @@
"dependencies": {
"strip-ansi": {
"version": "3.0.1",
"from": "strip-ansi@>=3.0.1 <4.0.0",
"from": "strip-ansi@>=3.0.0 <4.0.0",
"resolved": "http://beta-internal:4873/strip-ansi/-/strip-ansi-3.0.1.tgz",
"dependencies": {
"ansi-regex": {
......@@ -6726,7 +6731,7 @@
},
"strip-ansi": {
"version": "3.0.1",
"from": "strip-ansi@>=3.0.1 <4.0.0",
"from": "strip-ansi@>=3.0.0 <4.0.0",
"resolved": "http://beta-internal:4873/strip-ansi/-/strip-ansi-3.0.1.tgz",
"dependencies": {
"ansi-regex": {
......
......@@ -53,6 +53,7 @@
"gitter-web-elasticsearch": "file:modules/elasticsearch",
"gitter-web-env": "file:modules/env",
"gitter-web-fake-data": "file:modules/fake-data",
"gitter-web-fingerprinting": "file:modules/fingerprinting",
"gitter-web-forums": "file:modules/forums",
"gitter-web-github": "file:modules/github",
"gitter-web-github-backend": "file:modules/github-backend",
......@@ -158,7 +159,6 @@
"yargs": "^4.2.0"
},
"devDependencies": {
"inject-loader": "^3.0.0-beta1",
"@gitterhq/cal-heatmap": "^3.6.1",
"autoprefixer-core": "^5.1.0",
"babel-core": "^6.11.4",
......@@ -199,6 +199,7 @@
"fastdom": "^1.0.1",
"faye": "^1.1.2",
"file-loader": "^0.9.0",
"fingerprintjs2": "^1.4.1",
"fontfaceobserver": "^1.7.1",
"gitter-handlebars-loader": "^1.2.0",
"gitter-styleguide": "^1.6.6",
......@@ -233,6 +234,7 @@
"htmlclean": "^2.2.3",
"http-server": "^0.9.0",
"i18n-webpack-plugin": "^0.2.7",
"inject-loader": "^3.0.0-beta1",
"istanbul-instrumenter-loader": "^0.1.3",
"jquery": "2.1.0",
"jsdom": "^9.4.5",
......
'use strict';
var Promise = require('bluebird');
var apiClient = require('./api-client');
var cookies = require('../utils/cookies');
var MAX_FINGERPRINT_DURATION = 86400 * 1000; // one day
/**
* Fingerprint the current browser
*/
function getFingerprint() {
return new Promise(function(resolve) {
require.ensure(['fingerprintjs2'], function(require) {
var Fingerprint2 = require('fingerprintjs2');
new Fingerprint2({ })
.get(function(result) {
resolve(result);
});
})
});
}
function fingerprintValid(fpCookie) {
if (!fpCookie) return false;
var parts = fpCookie.split(':');
if (parts.length !== 2) return false;
var time = parseInt(parts[0], 10);
if (isNaN(time)) return false;
var duration = Date.now() - time;
if (duration < 0 || duration > MAX_FINGERPRINT_DURATION) return false;
if (!parts[1]) return false;
// TODO: possibly also check on the fingerprint
return true;
}
function captureFingerprint() {
var fingerprint = cookies.get('fp');
if (fingerprintValid(fingerprint)) {
return;
}
return getFingerprint()
.tap(function(fingerprint) {
return apiClient.priv.post('/fp', { fp: fingerprint }, {
dataType: 'text',
global: false
});
})
.tap(function(fingerprint) {
cookies.set('fp', '' + Date.now() + ':' + fingerprint);
});
}
module.exports = Promise.method(captureFingerprint);
......@@ -461,4 +461,10 @@ onready(function() {
});
}
setTimeout(function() {
var fingerprint = require('./components/fingerprint');
fingerprint();
}, 5000);
});
"use strict";
var fingerprintingService = require('gitter-web-fingerprinting/lib/fingerprinting-service');
module.exports = function(req, res, next) {
var userId = req.user._id;
var fingerprint = req.body && req.body.fp;
return fingerprintingService.recordFingerprint(userId, fingerprint, req.ip)
.then(function() {
res.send('OK');
})
.catch(next);
};
......@@ -123,4 +123,9 @@ router.post('/create-github-room',
identifyRoute('api-private-create-github-room'),
require('./create-github-room'));
router.post('/fp',
authMiddleware,
identifyRoute('api-private-fp'),
require('./fingerprint'));
module.exports = router;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment