diff --git a/logic/tracks.js b/logic/tracks.js index c272d78..3211758 100644 --- a/logic/tracks.js +++ b/logic/tracks.js @@ -254,4 +254,31 @@ function* parseObsver2(body) { } } -module.exports = { parseTrackPoints, detectFormat, parseObsver1, parseObsver2, replaceDollarNewlinesHack }; +/** + * This function normalizes a User-Agent header for storage in the database. It + * make sure that we only store the user-agent if it matches the pattern + * `OBS/*`, and extracts that part of the user agent, if it contains more + * information. This is the only part we are interested in, the + * remainder is too privacy sensitive to keep. + */ +function normalizeUserAgent(userAgent) { + if (!userAgent) { + return null; + } + + const match = userAgent.match(/\bOBS\/[^\s]+/); + if (match) { + return match[0]; + } + + return null; +} + +module.exports = { + detectFormat, + normalizeUserAgent, + parseObsver1, + parseObsver2, + parseTrackPoints, + replaceDollarNewlinesHack, +}; diff --git a/logic/tracks.test.js b/logic/tracks.test.js index dd8529f..b84a50b 100644 --- a/logic/tracks.test.js +++ b/logic/tracks.test.js @@ -1,4 +1,11 @@ -const { parseTrackPoints, parseObsver1, detectFormat, parseObsver2, replaceDollarNewlinesHack } = require('./tracks'); +const { + detectFormat, + normalizeUserAgent, + parseObsver1, + parseObsver2, + parseTrackPoints, + replaceDollarNewlinesHack, +} = require('./tracks'); const { test1, test2, test3 } = require('./_tracks_testdata'); @@ -92,3 +99,44 @@ describe('detectFormat', () => { expect(detectFormat('')).toBe('invalid'); }); }); + +describe('normalizeUserAgent', () => { + it('is a function', () => { + expect(typeof normalizeUserAgent).toBe('function'); + }); + + it('ignores falsy values', () => { + expect(normalizeUserAgent(null)).toBe(null); + expect(normalizeUserAgent('')).toBe(null); + }); + + it('ignores normal browser agents', () => { + const browserAgents = [ + 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 6P Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.83 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 6.0; HTC One M9 Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.98 Mobile Safari/537.3', + 'Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A5370a Safari/604.1', + 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1', + ]; + + for (const browserAgent of browserAgents) { + expect(normalizeUserAgent(browserAgent)).toBe(null); + } + }); + + it('detects OBS versions', () => { + const agents = ['OBS/123', 'OBS/2', 'OBS/1.2.3.4.5-rc123']; + + for (const agent of agents) { + expect(normalizeUserAgent(agent)).toBe(agent); + } + }); + + it('extracts OBS versions from extended formats', () => { + const agents = ['foo OBS/123', 'OBS/123 bar', 'foo OBS/123 bar']; + + for (const agent of agents) { + expect(normalizeUserAgent(agent)).toBe('OBS/123'); + } + }); +}); diff --git a/models/Track.js b/models/Track.js index 9d0b115..d445f33 100644 --- a/models/Track.js +++ b/models/Track.js @@ -9,6 +9,7 @@ const TrackSchema = new mongoose.Schema( description: String, body: String, visible: Boolean, + uploadedByUserAgent: String, numEvents: { type: Number, default: 0 }, comments: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }], author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, diff --git a/routes/api/tracks.js b/routes/api/tracks.js index 80092b0..6774e64 100644 --- a/routes/api/tracks.js +++ b/routes/api/tracks.js @@ -7,7 +7,7 @@ const User = mongoose.model('User'); const busboy = require('connect-busboy'); const auth = require('../auth'); const currentTracks = new Map(); -const { parseTrackPoints } = require('../../logic/tracks'); +const { parseTrackPoints, normalizeUserAgent } = require('../../logic/tracks'); const wrapRoute = require('../../_helpers/wrapRoute'); // Preload track objects on routes with ':track' @@ -216,6 +216,7 @@ router.post( if (track.body) { trackData.points = Array.from(parseTrackPoints(track.body)); + track.uploadedByUserAgent = normalizeUserAgent(req.headers['user-agent']); } track.visible = track.author.areTracksVisibleForAll; @@ -242,6 +243,7 @@ router.post( const trackData = new TrackData(); track.trackData = trackData._id; track.author = user; + track.uploadedByUserAgent = normalizeUserAgent(req.headers['user-agent']); await track.save(); await trackData.save(); @@ -372,6 +374,7 @@ router.put( req.track.trackData = trackData._id; } trackData.points = Array.from(parseTrackPoints(req.track.body)); + req.track.uploadedByUserAgent = normalizeUserAgent(req.headers['user-agent']); await trackData.save(); } diff --git a/routes/auth.js b/routes/auth.js index 1cf0dc8..e29fac8 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -2,7 +2,8 @@ const jwt = require('express-jwt'); const secret = require('../config').secret; function getTokenFromHeader(req) { - const [tokenType, token] = req.headers.authorization?.split(' ') || []; + const authorization = req.headers.authorization; + const [tokenType, token] = (authorization && authorization.split(' ')) || []; if (tokenType === 'Token' || tokenType === 'Bearer') { return token;