Only use user-agent data by default. Must explicitly call withClientHints() to also use client-hints data

This commit is contained in:
Faisal Salman 2023-03-14 23:22:08 +07:00
parent 60d3a2fbbc
commit f8dde65d54
2 changed files with 207 additions and 100 deletions

View File

@ -39,6 +39,11 @@
EMBEDDED = 'embedded', EMBEDDED = 'embedded',
USER_AGENT = 'user-agent', USER_AGENT = 'user-agent',
UA_MAX_LENGTH = 350, UA_MAX_LENGTH = 350,
BRANDS = 'brands',
FULLVERLIST = 'fullVersionList',
PLATFORM = 'platform',
PLATFORMVER = 'platformVersion',
BITNESS = 'bitness',
CH_HEADER = 'sec-ch-ua', CH_HEADER = 'sec-ch-ua',
CH_HEADER_FULL_VER_LIST = CH_HEADER + '-full-version-list', CH_HEADER_FULL_VER_LIST = CH_HEADER + '-full-version-list',
CH_HEADER_ARCH = CH_HEADER + '-arch', CH_HEADER_ARCH = CH_HEADER + '-arch',
@ -127,6 +132,16 @@
return /^(browser|cpu|device|engine|os)$/.test(prop); return /^(browser|cpu|device|engine|os)$/.test(prop);
} }
}, },
itemListToArray = function (header) {
if (!header) return undefined;
var arr = [];
var tokens = strip(/\\?\"/g, header).split(', ');
for (var i = 0; i < tokens.length; i++) {
var token = tokens[i].split(';v=');
arr[i] = { brand : token[0], version : token[1] };
}
return arr;
},
lowerize = function (str, rgx) { lowerize = function (str, rgx) {
return typeof(str) === STR_TYPE ? strip((rgx ? new RegExp(rgx, 'i') : EMPTY), str.toLowerCase()) : str; return typeof(str) === STR_TYPE ? strip((rgx ? new RegExp(rgx, 'i') : EMPTY), str.toLowerCase()) : str;
}, },
@ -136,6 +151,9 @@
strip = function (pattern, str) { strip = function (pattern, str) {
return str.replace(pattern, EMPTY); return str.replace(pattern, EMPTY);
}, },
stripQuotes = function (val) {
return typeof val === STR_TYPE ? strip(/\"/g, val) : val;
},
trim = function (str, len) { trim = function (str, len) {
if (typeof(str) === STR_TYPE) { if (typeof(str) === STR_TYPE) {
str = strip(/^\s\s*/, str); str = strip(/^\s\s*/, str);
@ -830,31 +848,21 @@
function UAParserDataCH (uach, isHTTP_UACH) { function UAParserDataCH (uach, isHTTP_UACH) {
uach = uach || {}; uach = uach || {};
initialize.call(this, CH_ALL_VALUES); initialize.call(this, CH_ALL_VALUES);
var setVal = function (val) {
return typeof val === STR_TYPE ? strip(/\"/g, val) : val || undefined;
};
if (isHTTP_UACH) { if (isHTTP_UACH) {
var toArray = function (header) { initialize.call(this, [
if (!header) return undefined; [BRANDS, itemListToArray(uach[CH_HEADER])],
var arr = []; [FULLVERLIST, itemListToArray(uach[CH_HEADER_FULL_VER_LIST])],
var tokens = strip(/\\?\"/g, header).split(', '); [BRANDS, itemListToArray(uach[CH_HEADER])],
for (var i = 0; i < tokens.length; i++) { [MOBILE, /\?1/.test(uach[CH_HEADER_MOBILE])],
var token = tokens[i].split(';v='); [MODEL, stripQuotes(uach[CH_HEADER_MODEL])],
arr[i] = { brand : token[0], version : token[1] }; [PLATFORM, stripQuotes(uach[CH_HEADER_PLATFORM])],
} [PLATFORMVER, stripQuotes(uach[CH_HEADER_PLATFORM_VER])],
return arr; [ARCHITECTURE, stripQuotes(uach[CH_HEADER_ARCH])],
}; [BITNESS, stripQuotes(uach[CH_HEADER_BITNESS])]
this.brands = toArray(uach[CH_HEADER]); ]);
this.fullVersionList = toArray(uach[CH_HEADER_FULL_VER_LIST]);
this.mobile = /\?1/.test(uach[CH_HEADER_MOBILE]);
this.model = setVal(uach[CH_HEADER_MODEL]);
this.platform = setVal(uach[CH_HEADER_PLATFORM]);
this.platformVersion = setVal(uach[CH_HEADER_PLATFORM_VER]);
this.architecture = setVal(uach[CH_HEADER_ARCH]);
this.bitness = setVal(uach[CH_HEADER_BITNESS]);
} else { } else {
for (var prop in uach) { for (var prop in uach) {
if(this.hasOwnProperty(prop) && uach[prop]) this[prop] = setVal(uach[prop]); if(this.hasOwnProperty(prop) && uach[prop]) this[prop] = stripQuotes(uach[prop]);
} }
} }
return this; return this;
@ -867,6 +875,7 @@
this.rgxMap = data[3]; this.rgxMap = data[3];
this.data = (function (data) { this.data = (function (data) {
var ua = data[0], var ua = data[0],
uaCH = data[1],
itemType = data[2], itemType = data[2],
rgxMap = data[3], rgxMap = data[3],
init_props = data[4], init_props = data[4],
@ -900,17 +909,41 @@
return str ? str : UNDEF_TYPE; return str ? str : UNDEF_TYPE;
}; };
UAParserData.prototype.withClientHints = function () { UAParserData.prototype.withClientHints = function () {
if (!NAVIGATOR_UADATA) return this; if (!NAVIGATOR_UADATA) {
var HTTP_UACH = uaCH;
switch (itemType) {
case UA_BROWSER:
return new UAParserBrowser(ua, rgxMap, HTTP_UACH).parseCH().get();
case UA_CPU:
return new UAParserCPU(ua, rgxMap, HTTP_UACH).parseCH().get();
case UA_DEVICE:
return new UAParserDevice(ua, rgxMap, HTTP_UACH).parseCH().get();
case UA_ENGINE:
return new UAParserEngine(ua, rgxMap).get();
case UA_OS:
return new UAParserOS(ua, rgxMap, HTTP_UACH).parseCH().get();
default :
return {
'ua' : ua,
'ua_ch' : uaCH,
'browser' : new UAParserBrowser(ua, rgxMap[UA_BROWSER], HTTP_UACH).parseCH().get(),
'cpu' : new UAParserCPU(ua, rgxMap[UA_CPU], HTTP_UACH).parseCH().get(),
'device' : new UAParserDevice(ua, rgxMap[UA_DEVICE], HTTP_UACH).parseCH().get(),
'engine' : new UAParserEngine(ua, rgxMap[UA_ENGINE]).get(),
'os' : new UAParserOS(ua, rgxMap[UA_OS], HTTP_UACH).parseCH().get()
};
}
}
return NAVIGATOR_UADATA return NAVIGATOR_UADATA
.getHighEntropyValues(CH_ALL_VALUES) .getHighEntropyValues(CH_ALL_VALUES)
.then(function (res) { .then(function (res) {
var JS_UACH = new UAParserDataCH(res, false), var JS_UACH = new UAParserDataCH(res, false),
browser = new UAParserBrowser(ua, rgxMap, JS_UACH).get(), browser = new UAParserBrowser(ua, rgxMap, JS_UACH).parseCH().get(),
cpu = new UAParserCPU(ua, ((itemType == UA_RESULT) ? rgxMap.cpu : rgxMap), JS_UACH).get(), cpu = new UAParserCPU(ua, ((itemType == UA_RESULT) ? rgxMap[UA_CPU] : rgxMap), JS_UACH).parseCH().get(),
device = new UAParserDevice(ua, rgxMap, JS_UACH).get(), device = new UAParserDevice(ua, rgxMap, JS_UACH).parseCH().get(),
engine = new UAParserEngine(ua, rgxMap).get(), engine = new UAParserEngine(ua, rgxMap).get(),
os = new UAParserOS(ua, rgxMap, JS_UACH).get(); os = new UAParserOS(ua, rgxMap, JS_UACH).parseCH().get();
switch (itemType) { switch (itemType) {
case UA_BROWSER: case UA_BROWSER:
@ -963,24 +996,21 @@
' ?browser$', ' ?browser$',
[NAME, VERSION] [NAME, VERSION]
]); ]);
this.parseCH(); this.parse();
if (!this.get(NAME)) { // Brave-specific detection
this.parse(); if (NAVIGATOR && NAVIGATOR.brave && typeof NAVIGATOR.brave.isBrave == FUNC_TYPE) {
// Brave-specific detection this.set(NAME, 'Brave');
if (NAVIGATOR && NAVIGATOR.brave && typeof NAVIGATOR.brave.isBrave == FUNC_TYPE) {
this.set(NAME, 'Brave');
}
} }
this.set(MAJOR, majorize(this.get(VERSION))); this.set(MAJOR, majorize(this.get(VERSION)));
} }
UAParserBrowser.prototype = new UAParserItem(); UAParserBrowser.prototype = new UAParserItem();
UAParserBrowser.prototype.parseCH = function () { UAParserBrowser.prototype.parseCH = function () {
var brands = this.uaCH.fullVersionList || this.uaCH.brands; var brands = this.uaCH[FULLVERLIST] || this.uaCH[BRANDS];
if (brands) { if (brands) {
for (var i in brands) { for (var i in brands) {
var brandName = brands[i].brand, var brandName = brands[i].brand,
brandVersion = brands[i].version; brandVersion = brands[i].version;
if (!/not.a.brand/i.test(brandName) && (!this.get(NAME) || /chromi/i.test(this.get(NAME)))) { if (!/not.a.brand/i.test(brandName) || /chromi/i.test(this.get(NAME))) {
this.set(NAME, strip(GOOGLE+' ', brandName)) this.set(NAME, strip(GOOGLE+' ', brandName))
.set(VERSION, brandVersion) .set(VERSION, brandVersion)
.set(MAJOR, majorize(brandVersion)); .set(MAJOR, majorize(brandVersion));
@ -1001,16 +1031,13 @@
null, null,
[ARCHITECTURE] [ARCHITECTURE]
]); ]);
this.parseCH(); this.parse();
if (!this.get(ARCHITECTURE)) {
this.parse();
}
} }
UAParserCPU.prototype = new UAParserItem(); UAParserCPU.prototype = new UAParserItem();
UAParserCPU.prototype.parseCH = function () { UAParserCPU.prototype.parseCH = function () {
var archName = this.uaCH.architecture; var archName = this.uaCH[ARCHITECTURE];
if (archName) { if (archName) {
archName += (archName && this.uaCH.bitness == '64') ? '64' : EMPTY; archName += (archName && this.uaCH[BITNESS] == '64') ? '64' : EMPTY;
rgxMapper.call(this.data, archName, this.rgxMap); rgxMapper.call(this.data, archName, this.rgxMap);
} }
return this; return this;
@ -1027,26 +1054,23 @@
null, null,
[VENDOR, MODEL] [VENDOR, MODEL]
]); ]);
this.parseCH(); this.parse();
if (!this.get(TYPE) || !this.get(MODEL)) { if (!this.get(TYPE) && NAVIGATOR_UADATA && NAVIGATOR_UADATA[MOBILE]) {
this.parse(); this.set(TYPE, MOBILE);
if (!this.get(TYPE) && NAVIGATOR_UADATA && NAVIGATOR_UADATA.mobile) { }
this.set(TYPE, MOBILE); // iPadOS-specific detection: identified as Mac, but has some iOS-only properties
} if (this.get(NAME) == 'Macintosh' && NAVIGATOR && typeof NAVIGATOR.standalone !== UNDEF_TYPE && NAVIGATOR.maxTouchPoints && NAVIGATOR.maxTouchPoints > 2) {
// iPadOS-specific detection: identified as Mac, but has some iOS-only properties this.set(MODEL, 'iPad')
if (this.get(NAME) == 'Macintosh' && NAVIGATOR && typeof NAVIGATOR.standalone !== UNDEF_TYPE && NAVIGATOR.maxTouchPoints && NAVIGATOR.maxTouchPoints > 2) { .set(TYPE, TABLET);
this.set(MODEL, 'iPad')
.set(TYPE, TABLET);
}
} }
} }
UAParserDevice.prototype = new UAParserItem(); UAParserDevice.prototype = new UAParserItem();
UAParserDevice.prototype.parseCH = function () { UAParserDevice.prototype.parseCH = function () {
if (this.uaCH.mobile) { if (this.uaCH[MOBILE]) {
this.set(TYPE, MOBILE); this.set(TYPE, MOBILE);
} }
if (this.uaCH.model) { if (this.uaCH[MODEL]) {
this.set(MODEL, strip(/\"/g, this.uaCH.model)); this.set(MODEL, this.uaCH[MODEL]);
} }
return this; return this;
}; };
@ -1077,21 +1101,18 @@
' ?os$', ' ?os$',
[NAME, VERSION] [NAME, VERSION]
]); ]);
this.parseCH(); this.parse();
if (!this.get(NAME)) { if (!this.get(NAME) && NAVIGATOR_UADATA && NAVIGATOR_UADATA[PLATFORM] && NAVIGATOR_UADATA[PLATFORM] != 'Unknown') {
this.parse(); this.set(NAME, NAVIGATOR_UADATA[PLATFORM]
if (!this.get(NAME) && NAVIGATOR_UADATA && NAVIGATOR_UADATA.platform && NAVIGATOR_UADATA.platform != 'Unknown') { .replace(/chrome os/i, CHROMIUM_OS)
this.set(NAME, NAVIGATOR_UADATA.platform .replace(/macos/i, MAC_OS)); // backward compatibility
.replace(/chrome os/i, CHROMIUM_OS)
.replace(/macos/i, MAC_OS)); // backward compatibility
}
} }
} }
UAParserOS.prototype = new UAParserItem(); UAParserOS.prototype = new UAParserItem();
UAParserOS.prototype.parseCH = function () { UAParserOS.prototype.parseCH = function () {
var osName = this.uaCH.platform; var osName = this.uaCH[PLATFORM];
if(osName) { if(osName) {
var osVersion = this.uaCH.platformVersion; var osVersion = this.uaCH[PLATFORMVER];
osVersion = (osName == WINDOWS) ? (parseInt(majorize(osVersion), 10) >= 13 ? '11' : '10') : osVersion; osVersion = (osName == WINDOWS) ? (parseInt(majorize(osVersion), 10) >= 13 ? '11' : '10') : osVersion;
this.set(NAME, osName) this.set(NAME, osName)
.set(VERSION, osVersion); .set(VERSION, osVersion);
@ -1141,23 +1162,23 @@
// public methods // public methods
this.getBrowser = function () { this.getBrowser = function () {
return new UAParserBrowser(userAgent, regexMap.browser, HTTP_UACH).get(); return new UAParserBrowser(userAgent, regexMap[UA_BROWSER], HTTP_UACH).get();
}; };
this.getCPU = function () { this.getCPU = function () {
return new UAParserCPU(userAgent, regexMap.cpu, HTTP_UACH).get(); return new UAParserCPU(userAgent, regexMap[UA_CPU], HTTP_UACH).get();
}; };
this.getDevice = function () { this.getDevice = function () {
return new UAParserDevice(userAgent, regexMap.device, HTTP_UACH).get(); return new UAParserDevice(userAgent, regexMap[UA_DEVICE], HTTP_UACH).get();
}; };
this.getEngine = function () { this.getEngine = function () {
return new UAParserEngine(userAgent, regexMap.engine).get(); return new UAParserEngine(userAgent, regexMap[UA_ENGINE]).get();
}; };
this.getOS = function () { this.getOS = function () {
return new UAParserOS(userAgent, regexMap.os, HTTP_UACH).get(); return new UAParserOS(userAgent, regexMap[UA_OS], HTTP_UACH).get();
}; };
this.getResult = function () { this.getResult = function () {

View File

@ -352,53 +352,133 @@ describe('Map UA-CH headers', function () {
'user-agent' : 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36' 'user-agent' : 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
}; };
const headers2 = { let uap = UAParser(headers).withClientHints();
'sec-ch-ua-mobile' : '?1', let browser = new UAParser(headers).getBrowser().withClientHints();
'user-agent' : 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36' let cpu = new UAParser(headers).getCPU().withClientHints();
let device = new UAParser(headers).getDevice().withClientHints();
let engine = new UAParser(headers).getEngine().withClientHints();
let os = new UAParser(headers).getOS().withClientHints();
let ua_ch = {
"architecture": "ARM",
"bitness": "64",
"brands": [
{
"brand": "Chromium",
"version": "93"
},
{
"brand": "Google Chrome",
"version": "93"
},
{
"brand": " Not;A Brand",
"version": "99"
}
],
"fullVersionList": [
{
"brand": "Chromium",
"version": "93.0.1.2"
},
{
"brand": "Google Chrome",
"version": "93.0.1.2"
},
{
"brand": " Not;A Brand",
"version": "99.0.1.2"
}
],
"mobile": true,
"model": "Pixel 99",
"platform": "Windows",
"platformVersion": "13"
}; };
let uap = UAParser(headers); it('Can read from client-hints headers using `withClientHints()`', function () {
let browser = uap.browser;
let cpu = uap.cpu;
let device = uap.device;
let engine = uap.engine;
let os = uap.os;
it('Can read from client-hints headers', function () {
assert.deepEqual(uap.ua_ch, ua_ch);
assert.strictEqual(uap.ua, "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36");
assert.strictEqual(uap.browser.name, "Chrome");
assert.strictEqual(uap.browser.version, "93.0.1.2");
assert.strictEqual(uap.browser.major, "93");
assert.strictEqual(browser.name, "Chrome"); assert.strictEqual(browser.name, "Chrome");
assert.strictEqual(browser.version, "93.0.1.2"); assert.strictEqual(browser.version, "93.0.1.2");
assert.strictEqual(browser.major, "93"); assert.strictEqual(browser.major, "93");
assert.strictEqual(uap.cpu.architecture, "arm64");
assert.strictEqual(cpu.architecture, "arm64"); assert.strictEqual(cpu.architecture, "arm64");
assert.strictEqual(uap.device.type, "mobile");
assert.strictEqual(uap.device.model, "Pixel 99");
assert.strictEqual(uap.device.vendor, undefined);
assert.strictEqual(device.type, "mobile"); assert.strictEqual(device.type, "mobile");
assert.strictEqual(device.model, "Pixel 99"); assert.strictEqual(device.model, "Pixel 99");
assert.strictEqual(device.vendor, undefined); assert.strictEqual(device.vendor, undefined);
assert.strictEqual(uap.engine.name, 'Blink');
assert.strictEqual(uap.engine.version, '110.0.0.0');
assert.strictEqual(engine.name, 'Blink'); assert.strictEqual(engine.name, 'Blink');
assert.strictEqual(engine.version, '110.0.0.0'); assert.strictEqual(engine.version, '110.0.0.0');
assert.strictEqual(uap.os.name, "Windows");
assert.strictEqual(uap.os.version, "11");
assert.strictEqual(os.name, "Windows"); assert.strictEqual(os.name, "Windows");
assert.strictEqual(os.version, "11"); assert.strictEqual(os.version, "11");
}); });
it('Can read from user-agent header', function () { it('Only read from user-agent header when called without `withClientHints()`', function () {
uap = UAParser(headers2); uap = UAParser(headers);
browser = uap.browser; browser = new UAParser(headers).getBrowser();
cpu = uap.cpu; cpu = new UAParser(headers).getCPU();
device = uap.device; device = new UAParser(headers).getDevice();
engine = uap.engine; engine = new UAParser(headers).getEngine();
os = uap.os; os = new UAParser(headers).getOS();
assert.strictEqual(browser.name, "Chrome"); assert.deepEqual(uap.ua_ch, ua_ch);
assert.strictEqual(browser.version, "110.0.0.0"); assert.strictEqual(uap.browser.name, "Chrome");
assert.strictEqual(browser.major, "110"); assert.strictEqual(uap.browser.version, "110.0.0.0");
assert.strictEqual(cpu.architecture, "amd64"); assert.strictEqual(uap.browser.major, "110");
assert.strictEqual(device.type, "mobile"); assert.strictEqual(uap.cpu.architecture, "amd64");
assert.strictEqual(device.model, undefined); assert.strictEqual(uap.device.type, undefined);
assert.strictEqual(device.vendor, undefined); assert.strictEqual(uap.device.model, undefined);
assert.strictEqual(engine.name, 'Blink'); assert.strictEqual(uap.device.vendor, undefined);
assert.strictEqual(engine.version, '110.0.0.0'); assert.strictEqual(uap.engine.name, 'Blink');
assert.strictEqual(os.name, "Linux"); assert.strictEqual(uap.engine.version, '110.0.0.0');
assert.strictEqual(os.version, "x86_64"); assert.strictEqual(uap.os.name, "Linux");
assert.strictEqual(uap.os.version, "x86_64");
});
it('Fallback to user-agent header when using `withClientHints()` but found no client hints-related headers', function () {
const headers2 = {
'sec-ch-ua-mobile' : '?1',
'user-agent' : 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
};
uap = UAParser(headers2).withClientHints();
ua_ch = {
"architecture": undefined,
"bitness": undefined,
"brands": undefined,
"fullVersionList": undefined,
"mobile": true,
"model": undefined,
"platform": undefined,
"platformVersion": undefined
};
assert.deepEqual(uap.ua_ch, ua_ch);
assert.strictEqual(uap.browser.name, "Chrome");
assert.strictEqual(uap.browser.version, "110.0.0.0");
assert.strictEqual(uap.browser.major, "110");
assert.strictEqual(uap.cpu.architecture, "amd64");
assert.strictEqual(uap.device.type, "mobile");
assert.strictEqual(uap.device.model, undefined);
assert.strictEqual(uap.device.vendor, undefined);
assert.strictEqual(uap.engine.name, 'Blink');
assert.strictEqual(uap.engine.version, '110.0.0.0');
assert.strictEqual(uap.os.name, "Linux");
assert.strictEqual(uap.os.version, "x86_64");
}); });
}); });
@ -451,6 +531,12 @@ describe('Map UA-CH JS', () => {
assert.strictEqual(result.cpu.architecture, 'amd64'); assert.strictEqual(result.cpu.architecture, 'amd64');
assert.strictEqual(result.os.name, 'Android'); assert.strictEqual(result.os.name, 'Android');
await uap.getDevice().withClientHints().then((device) => {
assert.strictEqual(device.type, 'mobile');
assert.strictEqual(device.vendor, undefined);
assert.strictEqual(device.model, 'Galaxy S3');
});
let result_without_ch = uap.getResult(); let result_without_ch = uap.getResult();
assert.strictEqual(result_without_ch.browser.name, undefined); assert.strictEqual(result_without_ch.browser.name, undefined);