Skip to content

Commit

Permalink
[helpers] Add new method: UACHParser(), parse client-hints HTTP hea…
Browse files Browse the repository at this point in the history
…ders into its JS API equivalent
  • Loading branch information
faisalman committed Aug 22, 2023
1 parent 3dd4b60 commit 1296576
Show file tree
Hide file tree
Showing 5 changed files with 297 additions and 59 deletions.
6 changes: 3 additions & 3 deletions src/helpers/package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"title": "UAParser.js Helpers",
"name": "@ua-parser-js/helpers",
"version": "0.0.1",
"version": "0.0.3",
"author": "Faisal Salman <f@faisalman.com>",
"description": "A collection of utility methods for UAParser.js",
"main": "ua-parser-helpers.js",
"module": "ua-parser-helpers.mjs",
"scripts" : {
"scripts": {
"test": "echo 1"
},
"repository": {
Expand All @@ -26,4 +26,4 @@
"url": "https://github.com/faisalman/ua-parser-js/issues"
},
"homepage": "https://github.com/faisalman/ua-parser-js#readme"
}
}
72 changes: 69 additions & 3 deletions src/helpers/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ Check whether a user-agent string match with [frozen user-agent pattern](https:/

### * `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

Expand Down Expand Up @@ -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'
*/

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'
```

```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"
}
*/
```
37 changes: 36 additions & 1 deletion src/helpers/ua-parser-helpers.d.ts
Original file line number Diff line number Diff line change
@@ -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 unfreezeUA(): Promise<string>;
export function unfreezeUA(): Promise<string>;
export function UACHParser(headers: ClientHintsHTTPHeaders): ClientHintsJSHighEntropy;
177 changes: 127 additions & 50 deletions src/helpers/ua-parser-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 unfreezeUA = async () => {
if (!navigator) {
throw new Error('Currently only support browser environment');
const unfreezeUA = async (ua, ch) => {
const env = typeof navigator == 'undefined' ? 'node' : 'browser';
if (env == 'node') {
if (!ua['user-agent']) {
throw new Error('User-Agent header not found');
}
ch = UACHParser(ua);
ua = ua['user-agent'];
} else {
let ua = navigator.userAgent;
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);
}
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;
}
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 = {
isFrozenUA,
unfreezeUA
unfreezeUA,
UACHParser
};
Loading

0 comments on commit 1296576

Please sign in to comment.