mirror of
https://github.com/faisalman/ua-parser-js.git
synced 2025-09-28 08:28:47 +03:00
[helpers] Add new method: UACHParser()
, parse client-hints HTTP headers into its JS API equivalent
This commit is contained in:
parent
3dd4b60ee9
commit
129657673b
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"title": "UAParser.js Helpers",
|
"title": "UAParser.js Helpers",
|
||||||
"name": "@ua-parser-js/helpers",
|
"name": "@ua-parser-js/helpers",
|
||||||
"version": "0.0.1",
|
"version": "0.0.3",
|
||||||
"author": "Faisal Salman <f@faisalman.com>",
|
"author": "Faisal Salman <f@faisalman.com>",
|
||||||
"description": "A collection of utility methods for UAParser.js",
|
"description": "A collection of utility methods for UAParser.js",
|
||||||
"main": "ua-parser-helpers.js",
|
"main": "ua-parser-helpers.js",
|
||||||
"module": "ua-parser-helpers.mjs",
|
"module": "ua-parser-helpers.mjs",
|
||||||
"scripts" : {
|
"scripts": {
|
||||||
"test": "echo 1"
|
"test": "echo 1"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -12,7 +12,11 @@ Check whether a user-agent string match with [frozen user-agent pattern](https:/
|
|||||||
|
|
||||||
### * `unfreezeUA():Promise<string>`
|
### * `unfreezeUA():Promise<string>`
|
||||||
|
|
||||||
construct new unfreezed user-agent string using real data from client hints
|
Construct new unfreezed user-agent string using real data from client hints
|
||||||
|
|
||||||
|
### * `UACHParser(headers: object): object`
|
||||||
|
|
||||||
|
Parse client hints HTTP headers (sec-ch-ua) into its JS API equivalent
|
||||||
|
|
||||||
## Code Example
|
## Code Example
|
||||||
|
|
||||||
@ -59,7 +63,69 @@ import { unfreezeUA } from '@ua-parser-js/helpers';
|
|||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Safari/537.36'
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Safari/537.36'
|
||||||
*/
|
*/
|
||||||
|
|
||||||
unfreezeUA();
|
unfreezeUA()
|
||||||
|
.then(ua => console.log(ua));
|
||||||
// 'Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) New Browser/110.1.2.3 Chromium/110.1.2.3 Safari/537.36'
|
// 'Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) New Browser/110.1.2.3 Chromium/110.1.2.3 Safari/537.36'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { UACHParser } from '@ua-parser-js/helpers';
|
||||||
|
|
||||||
|
/*
|
||||||
|
Suppose we're in a server having this client hints data:
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'sec-ch-ua' : '"Chromium";v="93", "Google Chrome";v="93", " Not;A Brand";v="99"',
|
||||||
|
'sec-ch-ua-full-version-list' : '"Chromium";v="93.0.1.2", "Google Chrome";v="93.0.1.2", " Not;A Brand";v="99.0.1.2"',
|
||||||
|
'sec-ch-ua-arch' : 'arm',
|
||||||
|
'sec-ch-ua-bitness' : '64',
|
||||||
|
'sec-ch-ua-mobile' : '?1',
|
||||||
|
'sec-ch-ua-model' : 'Pixel 99',
|
||||||
|
'sec-ch-ua-platform' : 'Linux',
|
||||||
|
'sec-ch-ua-platform-version' : '13',
|
||||||
|
'user-agent' : 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
const userAgentData = UACHParser(headers);
|
||||||
|
|
||||||
|
console.log(userAgentData);
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
"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": "Linux",
|
||||||
|
"platformVersion": "13"
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
```
|
35
src/helpers/ua-parser-helpers.d.ts
vendored
35
src/helpers/ua-parser-helpers.d.ts
vendored
@ -1,2 +1,37 @@
|
|||||||
|
interface IBrowser {
|
||||||
|
brand: string;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ClientHintsJSLowEntropy {
|
||||||
|
brands: Array<IBrowser>;
|
||||||
|
mobile: boolean;
|
||||||
|
platform: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClientHintsJSHighEntropy extends ClientHintsJSLowEntropy {
|
||||||
|
architecture?: string;
|
||||||
|
bitness?: string;
|
||||||
|
formFactor?: string;
|
||||||
|
fullVersionList?: Array<IBrowser>;
|
||||||
|
model?: string;
|
||||||
|
platformVersion?: string;
|
||||||
|
wow64?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ClientHintsHTTPHeaders {
|
||||||
|
'sec-ch-ua-arch'?: string;
|
||||||
|
'sec-ch-ua-bitness'?: string;
|
||||||
|
'sec-ch-ua'?: string;
|
||||||
|
'sec-ch-ua-form-factor'?: string;
|
||||||
|
'sec-ch-ua-full-version-list'?: string;
|
||||||
|
'sec-ch-ua-mobile'?: string;
|
||||||
|
'sec-ch-ua-model'?: string;
|
||||||
|
'sec-ch-ua-platform'?: string;
|
||||||
|
'sec-ch-ua-platform-version'?: string;
|
||||||
|
'sec-ch-ua-wow64'?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function isFrozenUA(ua: string): boolean;
|
export function isFrozenUA(ua: string): boolean;
|
||||||
export function unfreezeUA(): Promise<string>;
|
export function unfreezeUA(): Promise<string>;
|
||||||
|
export function UACHParser(headers: ClientHintsHTTPHeaders): ClientHintsJSHighEntropy;
|
@ -35,61 +35,138 @@
|
|||||||
|
|
||||||
const isFrozenUA = ua => /^Mozilla\/5\.0 \((Windows NT 10\.0; Win64; x64|Macintosh; Intel Mac OS X 10_15_7|X11; Linux x86_64|X11; CrOS x86_64 14541\.0\.0|Fuchsia|Linux; Android 10; K)\) AppleWebKit\/537\.36 \(KHTML, like Gecko\) Chrome\/\d+\.0\.0\.0 (Mobile )?Safari\/537\.36$/.test(ua);
|
const isFrozenUA = ua => /^Mozilla\/5\.0 \((Windows NT 10\.0; Win64; x64|Macintosh; Intel Mac OS X 10_15_7|X11; Linux x86_64|X11; CrOS x86_64 14541\.0\.0|Fuchsia|Linux; Android 10; K)\) AppleWebKit\/537\.36 \(KHTML, like Gecko\) Chrome\/\d+\.0\.0\.0 (Mobile )?Safari\/537\.36$/.test(ua);
|
||||||
|
|
||||||
const unfreezeUA = async () => {
|
const unfreezeUA = async (ua, ch) => {
|
||||||
if (!navigator) {
|
const env = typeof navigator == 'undefined' ? 'node' : 'browser';
|
||||||
throw new Error('Currently only support browser environment');
|
if (env == 'node') {
|
||||||
} else {
|
if (!ua['user-agent']) {
|
||||||
let ua = navigator.userAgent;
|
throw new Error('User-Agent header not found');
|
||||||
if (navigator.userAgentData && isFrozenUA(ua)) {
|
|
||||||
const ch = await navigator.userAgentData.getHighEntropyValues(['architecture', 'bitness', 'fullVersionList', 'model', 'platform', 'platformVersion', 'wow64']);
|
|
||||||
switch (ch.platform) {
|
|
||||||
case 'Windows':
|
|
||||||
let [major, minor] = ch.platformVersion?.split('.').map(i => parseInt(i, 10));
|
|
||||||
let osReplacer = (major < 1) ?
|
|
||||||
`$<OS> 6.${minor}` :
|
|
||||||
(major >= 13) ?
|
|
||||||
`$<OS> 11.${minor}` :
|
|
||||||
`$<OS> 10.${minor}`;
|
|
||||||
let archReplacer = (ch.architecture == 'arm') ?
|
|
||||||
'; ARM' :
|
|
||||||
(ch.wow64) ?
|
|
||||||
'; WOW64' :
|
|
||||||
(ch.architecture == 'x86' && ch.bitness == '64') ?
|
|
||||||
'; $<ARCH>' : '';
|
|
||||||
ua = ua.replace(/(?<OS>Windows NT) 10\.0/, osReplacer)
|
|
||||||
.replace(/; (?<ARCH>Win64; x64)/, archReplacer);
|
|
||||||
break;
|
|
||||||
case 'Android':
|
|
||||||
ua = ua.replace(/(?<OS>Android) 10; K/, `$<OS> ${ch.platformVersion}; ${ch.model}`);
|
|
||||||
break;
|
|
||||||
case 'Linux':
|
|
||||||
case 'Chrome OS':
|
|
||||||
archReplacer = (ch.architecture == 'arm') ?
|
|
||||||
((ch.bitness == '64') ? 'arm64' : 'arm') :
|
|
||||||
(ch.architecture == 'x86' && ch.bitness == '64') ?
|
|
||||||
'$<ARCH>' : 'x86';
|
|
||||||
|
|
||||||
ua = ua.replace(/(?<ARCH>x86_64)/, archReplacer);
|
|
||||||
break;
|
|
||||||
case 'macOS':
|
|
||||||
ua = ua.replace(/(?<OS>Mac OS X) 10_15_7/, `$<OS> ${ch.platformVersion.replace(/\./, '_')}`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let browserReplacer = '';
|
|
||||||
ch.fullVersionList?.forEach((browser) => {
|
|
||||||
if (!/not.a.brand/i.test(browser.brand)) {
|
|
||||||
browserReplacer += `${browser.brand}/${browser.version} `;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (browserReplacer) {
|
|
||||||
ua = ua.replace(/Chrome\/\d+\.0\.0\.0 /, browserReplacer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return ua;
|
ch = UACHParser(ua);
|
||||||
|
ua = ua['user-agent'];
|
||||||
|
} else {
|
||||||
|
ua = ua || navigator.userAgent;
|
||||||
|
ch = ch || await navigator.userAgentData?.getHighEntropyValues(['arch', 'bitness', 'fullVersionList', 'model', 'platform', 'platformVersion', 'wow64']);
|
||||||
}
|
}
|
||||||
|
if (isFrozenUA(ua) && ch) {
|
||||||
|
switch (ch.platform) {
|
||||||
|
case 'Windows':
|
||||||
|
let [major, minor] = ch.platformVersion
|
||||||
|
.split('.')
|
||||||
|
.map(num => parseInt(num, 10));
|
||||||
|
major = (major < 1) ? '6' : (major >= 13) ? '11' : '10';
|
||||||
|
ua = ua .replace(/(?<OS>Windows NT) 10\.0/, `$<OS> ${major}.${minor}`)
|
||||||
|
.replace(/; (?<ARCH>Win64; x64)/,
|
||||||
|
(ch.architecture == 'arm') ?
|
||||||
|
'; ARM' :
|
||||||
|
(ch.wow64) ?
|
||||||
|
'; WOW64' :
|
||||||
|
(ch.architecture == 'x86' && ch.bitness != '64') ?
|
||||||
|
'' : '; $<ARCH>');
|
||||||
|
break;
|
||||||
|
case 'Android':
|
||||||
|
ua = ua.replace(/(?<OS>Android) 10; K/, `$<OS> ${ch.platformVersion}; ${ch.model}`);
|
||||||
|
break;
|
||||||
|
case 'Linux':
|
||||||
|
case 'Chrome OS':
|
||||||
|
ua = ua.replace(/(?<ARCH>x86_64)/,
|
||||||
|
(ch.architecture == 'arm') ?
|
||||||
|
((ch.bitness == '64') ? 'arm64' : 'arm') :
|
||||||
|
(ch.architecture == 'x86' && ch.bitness != '64') ?
|
||||||
|
'x86' : '$<ARCH>');
|
||||||
|
break;
|
||||||
|
case 'macOS':
|
||||||
|
ua = ua.replace(/(?<OS>Mac OS X) 10_15_7/, `$<OS> ${ch.platformVersion.replace(/\./, '_')}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (ch.fullVersionList) {
|
||||||
|
ua = ua.replace(/Chrome\/\d+\.0\.0\.0 /,
|
||||||
|
ch.fullVersionList
|
||||||
|
.filter(browser => !/not.a.brand/i.test(browser.brand))
|
||||||
|
.map(browser => `${browser.brand.replace(/^google /i,'')}/${browser.version} `)
|
||||||
|
.join(''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ua;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UACHMap = {
|
||||||
|
'sec-ch-ua-arch' : {
|
||||||
|
prop : 'architecture',
|
||||||
|
type : 'sf-string'
|
||||||
|
},
|
||||||
|
'sec-ch-ua-bitness' : {
|
||||||
|
prop : 'bitness',
|
||||||
|
type : 'sf-string'
|
||||||
|
},
|
||||||
|
'sec-ch-ua' : {
|
||||||
|
prop : 'brands',
|
||||||
|
type : 'sf-list'
|
||||||
|
},
|
||||||
|
'sec-ch-ua-form-factor' : {
|
||||||
|
prop : 'formFactor',
|
||||||
|
type : 'sf-string'
|
||||||
|
},
|
||||||
|
'sec-ch-ua-full-version-list' : {
|
||||||
|
prop : 'fullVersionList',
|
||||||
|
type : 'sf-list'
|
||||||
|
},
|
||||||
|
'sec-ch-ua-mobile' : {
|
||||||
|
prop : 'mobile',
|
||||||
|
type : 'sf-boolean',
|
||||||
|
},
|
||||||
|
'sec-ch-ua-model' : {
|
||||||
|
prop : 'model',
|
||||||
|
type : 'sf-string',
|
||||||
|
},
|
||||||
|
'sec-ch-ua-platform' : {
|
||||||
|
prop : 'platform',
|
||||||
|
type : 'sf-string'
|
||||||
|
},
|
||||||
|
'sec-ch-ua-platform-version' : {
|
||||||
|
prop : 'platformVersion',
|
||||||
|
type : 'sf-string'
|
||||||
|
},
|
||||||
|
'sec-ch-ua-wow64' : {
|
||||||
|
prop : 'wow64',
|
||||||
|
type : 'sf-boolean'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const UACHParser = (headers) => {
|
||||||
|
const parse = (str, type) => {
|
||||||
|
if (!str) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
switch (type) {
|
||||||
|
case 'sf-boolean':
|
||||||
|
return /\?1/.test(str);
|
||||||
|
case 'sf-list':
|
||||||
|
return str.replace(/\\?\"/g, '')
|
||||||
|
.split(', ')
|
||||||
|
.map(brands => {
|
||||||
|
const [brand, version] = brands.split(';v=');
|
||||||
|
return {
|
||||||
|
brand : brand,
|
||||||
|
version : version
|
||||||
|
};
|
||||||
|
});
|
||||||
|
case 'sf-string':
|
||||||
|
default:
|
||||||
|
return str.replace(/\\?\"/g, '');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let ch = {};
|
||||||
|
Object.keys(UACHMap).forEach(field => {
|
||||||
|
if (headers.hasOwnProperty(field)) {
|
||||||
|
const { prop, type } = UACHMap[field];
|
||||||
|
ch[prop] = parse(headers[field], type);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return ch;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
isFrozenUA,
|
isFrozenUA,
|
||||||
unfreezeUA
|
unfreezeUA,
|
||||||
|
UACHParser
|
||||||
};
|
};
|
@ -1,7 +1,7 @@
|
|||||||
const { isFrozenUA } = require('@ua-parser-js/helpers');
|
const { isFrozenUA, unfreezeUA, UACHParser } = require('@ua-parser-js/helpers');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
|
|
||||||
describe('isFrozenUA', () => {
|
describe('isFrozenUA()', () => {
|
||||||
it('Returns whether a user agent is frozen', () => {
|
it('Returns whether a user agent is frozen', () => {
|
||||||
|
|
||||||
const regularWindowsUA = "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.1234.56 Safari/537.36";
|
const regularWindowsUA = "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.1234.56 Safari/537.36";
|
||||||
@ -33,3 +33,63 @@ describe('isFrozenUA', () => {
|
|||||||
assert.strictEqual(isFrozenUA(freezedTabletUA), true);
|
assert.strictEqual(isFrozenUA(freezedTabletUA), true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'sec-ch-ua' : '"Chromium";v="93", "Google Chrome";v="93", " Not;A Brand";v="99"',
|
||||||
|
'sec-ch-ua-full-version-list' : '"Chromium";v="93.0.1.2", "Google Chrome";v="93.0.1.2", " Not;A Brand";v="99.0.1.2"',
|
||||||
|
'sec-ch-ua-arch' : 'arm',
|
||||||
|
'sec-ch-ua-bitness' : '64',
|
||||||
|
'sec-ch-ua-mobile' : '?1',
|
||||||
|
'sec-ch-ua-model' : 'Pixel 99',
|
||||||
|
'sec-ch-ua-platform' : 'Linux',
|
||||||
|
'sec-ch-ua-platform-version' : '13',
|
||||||
|
'user-agent' : 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('unfreezeUA()', () => {
|
||||||
|
it('returns an unfreezed user-agent using real data from client hints HTTP headers (sec-ch-ua)', async () => {
|
||||||
|
const unfreezed = await unfreezeUA(headers);
|
||||||
|
assert.strictEqual(unfreezed, 'Mozilla/5.0 (X11; Linux arm64) AppleWebKit/537.36 (KHTML, like Gecko) Chromium/93.0.1.2 Chrome/93.0.1.2 Safari/537.36');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('UACHParser()', () => {
|
||||||
|
it('parse client hints HTTP headers (sec-ch-ua) into a JavaScript object', () => {
|
||||||
|
assert.deepEqual(UACHParser(headers), {
|
||||||
|
"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": "Linux",
|
||||||
|
"platformVersion": "13"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user