Faisal Salman 2023-09-09 19:32:28 +07:00
parent 385e0aaee5
commit 5226361348
21 changed files with 1 additions and 1124 deletions

View File

@ -1,13 +0,0 @@
{
"env": {
"browser": true,
"commonjs": true,
"es2021": true,
"node": true
},
"parserOptions": {
"ecmaVersion": "latest"
},
"rules": {
}
}

View File

@ -182,7 +182,6 @@
"@babel/traverse": "7.15.4", "@babel/traverse": "7.15.4",
"@jazzer.js/core": "^1.4.0", "@jazzer.js/core": "^1.4.0",
"@playwright/test": "~1.32.2", "@playwright/test": "~1.32.2",
"eslint": "^8.48.0",
"jshint": "~2.13.6", "jshint": "~2.13.6",
"mocha": "~8.2.0", "mocha": "~8.2.0",
"requirejs": "2.3.2", "requirejs": "2.3.2",
@ -219,10 +218,5 @@
"type": "github", "type": "github",
"url": "https://github.com/sponsors/faisalman" "url": "https://github.com/sponsors/faisalman"
} }
],
"workspaces": [
"src/gpu-detect",
"src/ua-client-hints",
"src/user-agent-helpers"
] ]
} }

View File

@ -46,24 +46,6 @@ const modules = [
dest : 'src/extensions/ua-parser-extensions.mjs', dest : 'src/extensions/ua-parser-extensions.mjs',
title : 'ua-parser-js/extensions', title : 'ua-parser-js/extensions',
replacements : [] replacements : []
},
{
src : 'src/gpu-detect/gpu-detect.js',
dest : 'src/gpu-detect/gpu-detect.mjs',
title : '@ua-parser-js/gpu-detect',
replacements : []
},
{
src : 'src/user-agent-helpers/user-agent-helpers.js',
dest : 'src/user-agent-helpers/user-agent-helpers.mjs',
title : '@ua-parser-js/user-agent-helpers',
replacements : []
},
{
src : 'src/ua-client-hints/ua-client-hints.js',
dest : 'src/ua-client-hints/ua-client-hints.mjs',
title : '@ua-parser-js/ua-client-hints',
replacements : []
} }
]; ];

View File

@ -9,7 +9,7 @@ echo '
- lint js code - lint js code
' '
npm run test:jshint || exit 1 npm run test:jshint || exit 1
npm run test:eslint || exit 1 #npm run test:eslint || exit 1
echo ' echo '
- test using mocha - test using mocha

View File

@ -1,3 +0,0 @@
export class GPUDetect {
static getGPU: { vendor: string, model: string }
}

View File

@ -1,116 +0,0 @@
//////////////////////////////////////////////
/* Extracts GPU information from user-agent
https://github.com/faisalman/ua-parser-js
Author: Faisal Salman <f@faisalman.com>
MIT License */
/////////////////////////////////////////////
/*jshint esversion: 11 */
const rendererMap = [
[[
/(intel).*\b(hd\sgraphics\s\d{4}|iris(?:\spro)|gma\s\w+)/i, // Intel
/(nvidia)\s(geforce\s(?:gtx?\s)\d\w+|quadro)/i, // NVIDIA
/\b(sis)\s(\w+)/i // SiS
], ['vendor', 'model']],
[[
/\b(radeon[\shdr\d]+\w{4,5})/i // ATI/AMD
], ['model', ['vendor', 'AMD']]],
[[
/(adreno\s(?:\(tm\)\s)\w+)/i // Qualcomm
], [['model', /\(tm\)\s/i, ''], ['vendor', 'Qualcomm']]]
];
const vendorMap = [
[[
/\b(amd|apple|arm|ati|img|intel|nvidia|qualcomm|samsung|sis)\b/i
], ['vendor']]
];
class RegexMap {
static parse(str, mapper) {
let res = {};
if (typeof str === 'string') {
for (const [regs, props] of mapper) {
if (!Array.isArray(regs)) {
throw new Error('RegexMap: Expect Array of RegExp');
}
if (!Array.isArray(props)) {
throw new Error('RegexMap: Expect Array for Properties Mapping');
}
for (const reg of regs) {
if (!reg instanceof RegExp) {
throw new Error('RegexMap: Expect RegExp Instance');
}
const matches = reg.exec(str);
if (matches) {
props.forEach((prop, idx) => {
const val = matches[idx+1];
if (Array.isArray(prop)) {
const key = prop[0];
if (typeof key !== 'string') {
throw new Error('RegexMap: Expect String Input');
}
if (prop.length == 2) {
if (typeof prop[1] === 'string') {
res[key] = prop[1];
} else if (typeof prop[1] === 'function') {
res[key] = prop[1].call(res, val);
}
} else if (prop.length == 3) {
if (prop[1] instanceof RegExp) {
res[key] = val.replace(prop[1], prop[2]);
} else if (typeof prop[1] === 'function') {
res[key] = prop[1].call(res, val, prop[2]);
}
} else if (prop.length == 4) {
res[key] = prop[3].call(res, val.replace(prop[1], prop[2]));
} else {
res[key] = val;
}
} else if (typeof prop === 'string') {
res[prop] = val;
}
});
if (res) return res;
}
}
}
}
return res;
};
}
class GPUDetect {
static getGPU (strRenderer, strVendor) {
let gpuInfo = { vendor : undefined, model : undefined };
if (typeof strRenderer !== 'string') {
if (globalThis.document) {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl2') ||
canvas.getContext('webgl') ||
canvas.getContext('experimental-webgl');
if (gl) {
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
strVendor = gl.getParameter(debugInfo?.UNMASKED_VENDOR_WEBGL);
strRenderer = gl.getParameter(debugInfo?.UNMASKED_RENDERER_WEBGL);
}
}
}
if (strRenderer || strVendor) {
({ vendor : gpuInfo.vendor, model : gpuInfo.model } = RegexMap.parse(strRenderer, rendererMap));
gpuInfo.vendor = gpuInfo.vendor ?? RegexMap.parse(strVendor, rendererMap)?.vendor ?? RegexMap.parse(strVendor, vendorMap)?.vendor;
}
return gpuInfo;
}
}
module.exports = {
GPUDetect
}

View File

@ -1,25 +0,0 @@
{
"title": "User-Agent GPU Info",
"name": "@ua-parser-js/gpu-detect",
"version": "0.0.1",
"author": "Faisal Salman <f@faisalman.com>",
"description": "Extracts GPU information from user-agent",
"main": "gpu-detect.js",
"module": "gpu-detect.mjs",
"scripts": {
"test": "mocha ../../test/mocha-test-helpers"
},
"repository": {
"type": "git",
"url": "git+https://github.com/faisalman/ua-parser-js.git"
},
"keywords": [
"ua-parser-js",
"gpu-detection"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/faisalman/ua-parser-js/issues"
},
"homepage": "https://github.com/faisalman/ua-parser-js#readme"
}

View File

@ -1,16 +0,0 @@
# @ua-parser-js/gpu-detect
This is a [UAParser.js](https://github.com/faisalman/ua-parser-js) module that extracts GPU information from user-agent.
```sh
npm i @ua-parser-js/gpu-detect
```
## Code Example
// in browser environment
const { vendor, model } = GPUDetect.getGPU();
// in non-browser environment
const { vendor, model } = GPUDetect.getGPU("AMD Radeon");
```

View File

@ -1,4 +0,0 @@
const { GPUDetect } = require('../gpu-detect.js');
console.log(GPUDetect.getGPU('AMD Radeon R9 M295X OpenGL Engine'));
console.log(GPUDetect.getGPU('','ATI Technologies Inc.'));

View File

@ -1,29 +0,0 @@
{
"title": "User-Agent Client Hints",
"name": "@ua-parser-js/ua-client-hints",
"version": "0.0.1",
"author": "Faisal Salman <f@faisalman.com>",
"description": "A collection of utility methods for working with user-agent client hints",
"main": "ua-client-hints.js",
"module": "ua-client-hints.mjs",
"scripts": {
"test": "mocha ./test/*"
},
"repository": {
"type": "git",
"url": "git+https://github.com/faisalman/ua-parser-js.git"
},
"keywords": [
"ua-parser-js",
"browser-detection",
"device-detection",
"os-detection",
"user-agent",
"client-hints"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/faisalman/ua-parser-js/issues"
},
"homepage": "https://github.com/faisalman/ua-parser-js#readme"
}

View File

@ -1,128 +0,0 @@
# @ua-parser-js/ua-client-hints
This is a [UAParser.js](https://github.com/faisalman/ua-parser-js) module that contains a collection of utility methods for working with user-agent client hints.
```sh
npm i @ua-parser-js/ua-client-hints
```
### * `getUAData([props:array]):object`
Get user-agent client hints values of current instance in form of JS object representation
### * `setUAData([uaData:object]):UAClientHints`
Set values of user-agent client hints for the current instance either from navigator.userAgentData or from HTTP headers (Sec-CH-UA-*)
### * `getSerializedUAData([props:array]):object`
Get user-agent client hints values of current instance in form of HTTP headers string representation (Sec-CH-UA-*)
## Code Example
```js
import { UAClientHints } from '@ua-parser-js/ua-client-hints';
/*
Suppose we're in a server having this client hints data:
const httpHeaders = {
'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'
};
*/
const uaCH = new UAClientHints();
uaCH.setUAData(httpHeaders);
const uaCHData1 = uaCH.getUAData();
const uaCHData2 = uaCH.getUAData(['architecture', 'bitness']);
console.log(uaCHData1);
/*
{
"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",
"wow64": null,
"formFactor": null
}
*/
console.log(uaCHData2);
/*
{
"architecture": "arm",
"bitness": "64"
}
*/
uaCH.setUAData({
"wow64" : true,
"formFactor" : "Automotive"
});
const headersData1 = uaCH.getSerializedUAData();
const headersData2 = uaCH.getSerializedUAData(['brand', 'mobile', 'model']);
console.log(headersData1);
/*
{
'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',
'sec-ch-ua-wow64' : '?1',
'sec-ch-ua-form-factor' : 'Automotive'
};
*/
console.log(headersData2);
/*
{
'sec-ch-ua' : '"Chromium";v="93", "Google Chrome";v="93", " Not;A Brand";v="99"',
'sec-ch-ua-mobile' : '?1',
'sec-ch-ua-model' : 'Pixel 99'
};
*/
```

View File

@ -1,64 +0,0 @@
const { UAClientHints } = require('../ua-client-hints');
const assert = require('assert');
describe('Parse CH Headers', () => {
it('parse client hints HTTP headers (sec-ch-ua) into a client hints-like JavaScript object', () => {
const req = {
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'
}};
assert.deepEqual(new UAClientHints().setUAData(req.headers).getUAData(['architecture', 'bitness']), {
"architecture": "arm",
"bitness": "64"
});
assert.deepEqual(new UAClientHints().setUAData(req.headers).getUAData(), {
"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"
}
],
"formFactor": null,
"mobile": true,
"model": "Pixel 99",
"platform": "Linux",
"platformVersion": "13",
"wow64": null
});
});
});

View File

@ -1,39 +0,0 @@
export type UABrowser = {
brand: string | null,
version: string | null
};
export type UADataType = boolean | string | Array<UABrowser> | null;
export type UADataField =
'brands' |
'mobile' |
'platform' |
'architecture' |
'bitness' |
'formFactor' |
'fullVersionList' |
'model' |
'platformVersion' |
'wow64';
export type HeaderType = 'sf-boolean' | 'sf-string' | 'sf-list';
export type HeaderField =
'sec-ch-ua-arch' |
'sec-ch-ua-bitness' |
'sec-ch-ua' |
'sec-ch-ua-form-factor' |
'sec-ch-ua-full-version-list' |
'sec-ch-ua-mobile' |
'sec-ch-ua-model' |
'sec-ch-ua-platform' |
'sec-ch-ua-platform-version' |
'sec-ch-ua-wow64';
export class UAClientHints {
#ch: Map<UADataField, UADataType>;
#parseHeader(str: string, type: HeaderType): UADataType;
#serialize(data: UADataType, type: HeaderType): string;
getSerializedUAData(): Record<HeaderField, string>;
getUAData(props?: Array<UADataField>): Record<UADataField, UADataType>;
setUAData(uaData: Record<UADataField, UADataType> | Record<HeaderField, string>): UAClientHints;
};

View File

@ -1,130 +0,0 @@
//////////////////////////////////////////////
/* A collection of utility methods for
working with user-agent client hints
https://github.com/faisalman/ua-parser-js
Author: Faisal Salman <f@faisalman.com>
MIT License */
/////////////////////////////////////////////
/*jshint esversion: 11 */
const fieldType = Object.freeze({
Boolean : 'sf-boolean',
List : 'sf-list',
String : 'sf-string'
});
const uaCHMap = Object.freeze({
architecture : {
field : 'Sec-CH-UA-Arch',
type : fieldType.String
},
bitness : {
field : 'Sec-CH-UA-Bitness',
type : fieldType.String
},
brands : {
field : 'Sec-CH-UA',
type : fieldType.List
},
formFactor : {
field : 'Sec-CH-UA-Form-Factor',
type : fieldType.String
},
fullVersionList : {
field : 'Sec-CH-UA-Full-Version-List',
type : fieldType.List
},
mobile : {
field : 'Sec-CH-UA-Mobile',
type : fieldType.Boolean
},
model : {
field : 'Sec-CH-UA-Model',
type : fieldType.String
},
platform : {
field : 'Sec-CH-UA-Platform',
type : fieldType.String
},
platformVersion : {
field : 'Sec-CH-UA-Platform-Version',
type : fieldType.String
},
wow64 : {
field : 'Sec-CH-UA-WOW64',
type : fieldType.Boolean
}
});
class UAClientHints {
#uach = new Map();
constructor () {
for (const key in uaCHMap) {
this.#uach.set(key, null);
}
return this;
};
#parseHeader (str, type) {
if (!str) {
return null;
}
switch (type) {
case fieldType.Boolean:
return /\?1/.test(str);
case fieldType.List:
return str.replace(/\\?\"/g, '')
.split(',')
.map(brands => {
const [brand, version] = brands.trim().split(';v=');
return {
brand : brand,
version : version
};
});
case fieldType.String:
return str.replace(/\s*\\?\"\s*/g, '');
default:
return '';
}
};
#serialize(data, type) {
throw new Error('Not implemented yet');
//return '';
}
getSerializedUAData() {
throw new Error('Not implemented yet');
//let http = {};
//return http;
}
getUAData(props) {
if (props) {
return Object.fromEntries(props.filter(val => this.#uach.get(val)).map(val => [val, this.#uach.get(val)]));
}
return Object.fromEntries(this.#uach);
}
setUAData(uaDataValues) {
if(Object.keys(uaDataValues).some(key => key.startsWith('sec-ch-ua'))) {
for (const val in uaCHMap) {
const { field, type } = uaCHMap[val];
this.#uach.set(val, this.#parseHeader(uaDataValues[field.toLowerCase()], type));
}
} else {
for (const value in uaDataValues) {
if (this.#uach.has(value)) this.#uach.set(value, uaDataValues[value]);
}
}
return this;
};
}
module.exports = {
UAClientHints
};

View File

@ -1,134 +0,0 @@
// Generated ESM version of @ua-parser-js/ua-client-hints
// DO NOT EDIT THIS FILE!
// Source: /src/ua-client-hints/ua-client-hints.js
//////////////////////////////////////////////
/* A collection of utility methods for
working with user-agent client hints
https://github.com/faisalman/ua-parser-js
Author: Faisal Salman <f@faisalman.com>
MIT License */
/////////////////////////////////////////////
/*jshint esversion: 11 */
const fieldType = Object.freeze({
Boolean : 'sf-boolean',
List : 'sf-list',
String : 'sf-string'
});
const uaCHMap = Object.freeze({
architecture : {
field : 'Sec-CH-UA-Arch',
type : fieldType.String
},
bitness : {
field : 'Sec-CH-UA-Bitness',
type : fieldType.String
},
brands : {
field : 'Sec-CH-UA',
type : fieldType.List
},
formFactor : {
field : 'Sec-CH-UA-Form-Factor',
type : fieldType.String
},
fullVersionList : {
field : 'Sec-CH-UA-Full-Version-List',
type : fieldType.List
},
mobile : {
field : 'Sec-CH-UA-Mobile',
type : fieldType.Boolean
},
model : {
field : 'Sec-CH-UA-Model',
type : fieldType.String
},
platform : {
field : 'Sec-CH-UA-Platform',
type : fieldType.String
},
platformVersion : {
field : 'Sec-CH-UA-Platform-Version',
type : fieldType.String
},
wow64 : {
field : 'Sec-CH-UA-WOW64',
type : fieldType.Boolean
}
});
class UAClientHints {
#uach = new Map();
constructor () {
for (const key in uaCHMap) {
this.#uach.set(key, null);
}
return this;
};
#parseHeader (str, type) {
if (!str) {
return null;
}
switch (type) {
case fieldType.Boolean:
return /\?1/.test(str);
case fieldType.List:
return str.replace(/\\?\"/g, '')
.split(',')
.map(brands => {
const [brand, version] = brands.trim().split(';v=');
return {
brand : brand,
version : version
};
});
case fieldType.String:
return str.replace(/\s*\\?\"\s*/g, '');
default:
return '';
}
};
#serialize(data, type) {
throw new Error('Not implemented yet');
//return '';
}
getSerializedUAData() {
throw new Error('Not implemented yet');
//let http = {};
//return http;
}
getUAData(props) {
if (props) {
return Object.fromEntries(props.filter(val => this.#uach.get(val)).map(val => [val, this.#uach.get(val)]));
}
return Object.fromEntries(this.#uach);
}
setUAData(uaDataValues) {
if(Object.keys(uaDataValues).some(key => key.startsWith('sec-ch-ua'))) {
for (const val in uaCHMap) {
const { field, type } = uaCHMap[val];
this.#uach.set(val, this.#parseHeader(uaDataValues[field.toLowerCase()], type));
}
} else {
for (const value in uaDataValues) {
if (this.#uach.has(value)) this.#uach.set(value, uaDataValues[value]);
}
}
return this;
};
}
export {
UAClientHints
};

View File

@ -1,32 +0,0 @@
{
"title": "User-Agent Helpers",
"name": "@ua-parser-js/user-agent-helpers",
"version": "0.0.2",
"author": "Faisal Salman <f@faisalman.com>",
"description": "A collection of utility methods for working with user-agent",
"main": "user-agent-helpers.js",
"module": "user-agent-helpers.mjs",
"scripts": {
"test": "mocha ./test/*"
},
"repository": {
"type": "git",
"url": "git+https://github.com/faisalman/ua-parser-js.git"
},
"keywords": [
"ua-parser-js",
"browser-detection",
"device-detection",
"os-detection",
"user-agent",
"client-hints"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/faisalman/ua-parser-js/issues"
},
"homepage": "https://github.com/faisalman/ua-parser-js#readme",
"dependencies": {
"@ua-parser-js/client-hints-helpers": "*"
}
}

View File

@ -1,77 +0,0 @@
# @ua-parser-js/user-agent-helpers
This is a [UAParser.js](https://github.com/faisalman/ua-parser-js) module that contains a collection of utility methods for working with user-agent.
```sh
npm i @ua-parser-js/user-agent-helpers
```
### * `isFrozenUA(ua:string):boolean`
Check whether a user-agent string match with [frozen user-agent pattern](https://www.chromium.org/updates/ua-reduction/)
### * `unfreezeUA([ua:string,ch:object]|[headers:object]):Promise<string>`
Construct new unfreezed user-agent string using real data from client hints
## Code Example
```js
import { isFrozenUA } from '@ua-parser-js/user-agent-helpers';
const regularMobileUA = "Mozilla/5.0 (Linux; Android 9; SM-A205U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.1234.56 Mobile Safari/537.36";
const freezedMobileUA = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Mobile Safari/537.36";
console.log(isFrozenUA(regularMobileUA));
// false
console.log(isFrozenUA(freezedMobileUA));
// true
```
```js
import { unfreezeUA } from '@ua-parser-js/user-agent-helpers';
/*
Suppose we're in a browser having this client hints data:
{
fullVersionList: [
{
brand: 'New Browser',
version: '110.1.2.3'
},
{
brand: 'Chromium',
version: '110.1.2.3'
},
{
brand: 'Not(A:Brand',
version: '110'
}
],
platform: 'Windows',
platformVersion: '13.0.0',
architecture: 'arm'
}
With a frozen user-agent:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Safari/537.36'
*/
// Now let's generate a complete user-agent:
unfreezeUA()
.then(newUA => console.log(newUA));
// 'Mozilla/5.0 (Windows NT 11.0; ARM) AppleWebKit/537.36 (KHTML, like Gecko) New Browser/110.1.2.3 Chromium/110.1.2.3 Safari/537.36'
/*
// Alternatively:
const ua = navigator.userAgent;
const ch = await navigator.userAgentData.getHighEntropyValues();
const newUA = await unfreezeUA(ua, ch);
// Server environment:
const newUA = await unfreezeUA(req.headers);
*/
```

View File

@ -1,56 +0,0 @@
const { isFrozenUA, unfreezeUA } = require('../user-agent-helpers');
const { UAClientHints } = require('@ua-parser-js/ua-client-hints');
const assert = require('assert');
describe('isFrozenUA()', () => {
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 freezedWindowsUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Safari/537.36";
const regularMacUA = "";
const freezedMacUA = "";
const regularLinuxUA = "";
const freezedLinuxUA = "";
const regularCrOSUA = "";
const freezedCrOSUA = "";
const regularFuchsiaUA = "";
const freezedFuchsiaUA = "";
const regularMobileUA = "Mozilla/5.0 (Linux; Android 9; SM-A205U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.1234.56 Mobile Safari/537.36";
const freezedMobileUA = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Mobile Safari/537.36";
const regularTabletUA = "Mozilla/5.0 (Linux; Android 9; SM-T810) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.1234.56 Safari/537.36";
const freezedTabletUA = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Safari/537.36";
assert.strictEqual(isFrozenUA(regularWindowsUA), false);
assert.strictEqual(isFrozenUA(freezedWindowsUA), true);
assert.strictEqual(isFrozenUA(regularMobileUA), false);
assert.strictEqual(isFrozenUA(freezedMobileUA), true);
assert.strictEqual(isFrozenUA(regularTabletUA), false);
assert.strictEqual(isFrozenUA(freezedTabletUA), true);
});
});
const req = {
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(req.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');
});
});

View File

@ -1,35 +0,0 @@
export type UABrowser = {
brand: string | null,
version: string | null
};
export type UADataType = boolean | string | Array<UABrowser> | null;
export type UADataField =
'brands' |
'mobile' |
'platform' |
'architecture' |
'bitness' |
'formFactor' |
'fullVersionList' |
'model' |
'platformVersion' |
'wow64';
export type HeaderType = 'sf-boolean' | 'sf-string' | 'sf-list';
export type HeaderField =
'sec-ch-ua-arch' |
'sec-ch-ua-bitness' |
'sec-ch-ua' |
'sec-ch-ua-form-factor' |
'sec-ch-ua-full-version-list' |
'sec-ch-ua-mobile' |
'sec-ch-ua-model' |
'sec-ch-ua-platform' |
'sec-ch-ua-platform-version' |
'sec-ch-ua-wow64';
export function isFrozenUA(ua: string): boolean;
export function unfreezeUA(): Promise<string>;
export function unfreezeUA(ua: string, ch: Record<UADataField, UADataType>): Promise<string>;
export function unfreezeUA(headers: Record<HeaderField, HeaderType>): Promise<string>;

View File

@ -1,97 +0,0 @@
////////////////////////////////////////////////////
/* A collection of utility methods for user-agent
https://github.com/faisalman/ua-parser-js
Author: Faisal Salman <f@faisalman.com>
MIT License */
///////////////////////////////////////////////////
/*jshint esversion: 11 */
const { UAClientHints } = require('@ua-parser-js/ua-client-hints');
/*
# Reference:
https://www.chromium.org/updates/ua-reduction/
# Desktop
---
Format:
Mozilla/5.0 (<unifiedPlatform>) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/<majorVersion>.0.0.0 Safari/537.36
Possible <unifiedPlatform> values:
- 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
# Mobile & Tablet: (except iOS/Android WebView)
---
Format:
Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/<majorVersion>.0.0.0 <deviceCompat> Safari/537.36
Possible <deviceCompat> values:
- "Mobile"
- "" (empty string for Tablets & Desktop)
*/
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 (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 = new UAClientHints().setUAData(ua).getUAData();
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;
};
module.exports = {
isFrozenUA,
unfreezeUA
};

View File

@ -1,101 +0,0 @@
// Generated ESM version of @ua-parser-js/user-agent-helpers
// DO NOT EDIT THIS FILE!
// Source: /src/user-agent-helpers/user-agent-helpers.js
////////////////////////////////////////////////////
/* A collection of utility methods for user-agent
https://github.com/faisalman/ua-parser-js
Author: Faisal Salman <f@faisalman.com>
MIT License */
///////////////////////////////////////////////////
/*jshint esversion: 11 */
import { UAClientHints } from '@ua-parser-js/ua-client-hints';
/*
# Reference:
https://www.chromium.org/updates/ua-reduction/
# Desktop
---
Format:
Mozilla/5.0 (<unifiedPlatform>) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/<majorVersion>.0.0.0 Safari/537.36
Possible <unifiedPlatform> values:
- 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
# Mobile & Tablet: (except iOS/Android WebView)
---
Format:
Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/<majorVersion>.0.0.0 <deviceCompat> Safari/537.36
Possible <deviceCompat> values:
- "Mobile"
- "" (empty string for Tablets & Desktop)
*/
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 (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 = new UAClientHints().setUAData(ua).getUAData();
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;
};
export {
isFrozenUA,
unfreezeUA
};