@softvisio/core
Version:
Softisio core
653 lines (529 loc) • 17.9 kB
JavaScript
import CacheLru from "#lib/cache/lru";
import { readConfigSync } from "#lib/config";
import externalResources from "#lib/external-resources";
const CACHE = new CacheLru( { "maxSize": 1000 } ),
RESOURCE = await externalResources
.add( "softvisio-node/core/resources/user-agent" )
.on( "update", resource => {
CACHE.clear();
resource.data = null;
} )
.check(),
CHROMIUM_BRANDS = {
"chromium": {
"id": "chromium",
"name": "Chromium",
"fullName": "Chromium",
},
"chrome": {
"id": "chrome",
"name": "Chrome",
"fullName": "Google Chrome",
},
"msedge": {
"id": "msedge",
"name": "Edge",
"fullName": "Microsoft Edge",
},
},
BROWSER_FAMILY_BRANDS = {
"chromium": CHROMIUM_BRANDS[ "chromium" ],
"chrome": CHROMIUM_BRANDS[ "chrome" ],
"chrome mobile": CHROMIUM_BRANDS[ "chrome" ],
"msedge": CHROMIUM_BRANDS[ "msedge" ],
"edge": CHROMIUM_BRANDS[ "msedge" ],
"edge mobile": CHROMIUM_BRANDS[ "msedge" ],
},
PLATFORMS = {
"android": {
"id": "android",
"mobile": true,
"chromium": "Linux; Android 10; K",
"navigator": "Linux armv81",
"sec-ch-ua-platform": "Android",
},
"ios": {
"id": "ios",
"mobile": true,
"chromium": null,
"navigator": "iOS",
"sec-ch-ua-platform": "iOS",
"webgl": {
"vendor": "Apple Inc.",
"renderer": "Apple GPU",
},
},
"linux": {
"id": "linux",
"mobile": false,
"chromium": "X11; Linux x86_64",
"navigator": "Linux x86_64",
"sec-ch-ua-platform": "Linux",
},
"macos": {
"id": "macos",
"mobile": false,
"chromium": "Macintosh; Intel Mac OS X 10_15_7",
"navigator": "MacIntel",
"sec-ch-ua-platform": "macOS",
},
"windows": {
"id": "windows",
"mobile": false,
"chromium": "Windows NT 10.0; Win64; x64",
"navigator": "Win32",
"sec-ch-ua-platform": "Windows",
},
},
OS_FAMILY_PLATFORM = {
"android": PLATFORMS[ "android" ],
"ios": PLATFORMS[ "ios" ],
"linux": PLATFORMS[ "linux" ],
"macos": PLATFORMS[ "macos" ],
"mac os x": PLATFORMS[ "macos" ],
"windows": PLATFORMS[ "windows" ],
},
WEBGL_DEFAULT = {
"vendor": "Intel Inc.",
"renderer": "Intel Iris OpenGL Engine",
},
CHROMIUM_BRANDS_ORDER = {
"1": [
[ 0, 1 ],
[ 1, 0 ],
],
"2": [
[ 0, 1, 2 ],
[ 0, 2, 1 ],
[ 1, 0, 2 ],
[ 1, 2, 0 ],
[ 2, 0, 1 ],
[ 2, 1, 0 ],
],
"3": [
[ 0, 1, 2, 3 ],
[ 0, 1, 3, 2 ],
[ 0, 2, 1, 3 ],
[ 0, 2, 3, 1 ],
[ 0, 3, 1, 2 ],
[ 0, 3, 2, 1 ],
[ 1, 0, 2, 3 ],
[ 1, 0, 3, 2 ],
[ 1, 2, 0, 3 ],
[ 1, 2, 3, 0 ],
[ 1, 3, 0, 2 ],
[ 1, 3, 2, 0 ],
[ 2, 0, 1, 3 ],
[ 2, 0, 3, 1 ],
[ 2, 1, 0, 3 ],
[ 2, 1, 3, 0 ],
[ 2, 3, 0, 1 ],
[ 2, 3, 1, 0 ],
[ 3, 0, 1, 2 ],
[ 3, 0, 2, 1 ],
[ 3, 1, 0, 2 ],
[ 3, 1, 2, 0 ],
[ 3, 2, 0, 1 ],
[ 3, 2, 1, 0 ],
],
};
function getResources ( name ) {
RESOURCE.data ??= readConfigSync( RESOURCE.getResourcePath( "regexes.json" ) );
if ( !( RESOURCE.data[ name ][ 0 ][ 0 ] instanceof RegExp ) ) {
for ( const row of RESOURCE.data[ name ] ) {
if ( Array.isArray( row[ 0 ] ) ) {
row[ 0 ] = new RegExp( ...row[ 0 ] );
}
else {
row[ 0 ] = new RegExp( row[ 0 ] );
}
}
}
return RESOURCE.data[ name ];
}
class UserAgentData {
#userAgent;
constructor ( userAgent ) {
this.#userAgent = userAgent;
}
// properties
get userAgent () {
return this.#userAgent;
}
}
class UserAgentBrowser extends UserAgentData {
#parsed;
#family = null;
#majorVersion = null;
#version = null;
#name;
// properties
get family () {
if ( !this.#parsed ) this.#parse();
return this.#family;
}
get majorVersion () {
if ( !this.#parsed ) this.#parse();
return this.#majorVersion;
}
get version () {
if ( !this.#parsed ) this.#parse();
return this.#version;
}
get name () {
if ( this.#name === undefined ) {
this.#name = null;
if ( this.family ) {
this.#name = this.family;
if ( this.version ) {
this.#name += " " + this.version;
}
}
}
return this.#name;
}
// public
toString () {
return this.name;
}
toJSON () {
return {
"family": this.family,
"majorVersion": this.majorVersion,
"version": this.version,
};
}
// private
#parse () {
if ( this.#parsed ) return;
this.#parsed = true;
const userAgent = this.userAgent.userAgent;
if ( !userAgent ) return;
for ( const row of getResources( "browser" ) ) {
const match = row[ 0 ].exec( userAgent );
if ( match ) {
this.#family = row[ 1 ]?.replaceAll( /\$(\d+)/g, ( string, index ) => match[ index ] ) ?? match[ 1 ];
const major = row[ 2 ]?.replaceAll( /\$(\d+)/g, ( string, index ) => match[ index ] ) ?? match[ 2 ] ?? null,
minor = row[ 3 ]?.replaceAll( /\$(\d+)/g, ( string, index ) => match[ index ] ) ?? match[ 3 ] ?? null,
patch = row[ 4 ]?.replaceAll( /\$(\d+)/g, ( string, index ) => match[ index ] ) ?? match[ 4 ] ?? null,
build = row[ 5 ]?.replaceAll( /\$(\d+)/g, ( string, index ) => match[ index ] ) ?? match[ 5 ] ?? null;
if ( major == null ) {
this.#majorVersion = null;
this.#version = null;
}
else {
this.#majorVersion = +major;
this.#version = major;
if ( minor != null ) {
this.#version += "." + minor;
if ( patch != null ) {
this.#version += "." + patch;
if ( build != null ) {
this.#version += "." + build;
}
}
}
}
break;
}
}
}
}
class UserAgentOs extends UserAgentData {
#parsed;
#family = null;
#majorVersion = null;
#version = null;
#name;
// properties
get family () {
if ( !this.#parsed ) this.#parse();
return this.#family;
}
get majorVersion () {
if ( !this.#parsed ) this.#parse();
return this.#majorVersion;
}
get version () {
if ( !this.#parsed ) this.#parse();
return this.#version;
}
get name () {
if ( this.#name === undefined ) {
this.#name = null;
if ( this.family ) {
this.#name = this.family;
if ( this.version ) {
this.#name += " " + this.version;
}
}
}
return this.#name;
}
// public
toString () {
return this.name;
}
toJSON () {
return {
"family": this.family,
"majorVersion": this.majorVersion,
"version": this.version,
};
}
// private
#parse () {
if ( this.#parsed ) return;
this.#parsed = true;
const userAgent = this.userAgent.userAgent;
if ( !userAgent ) return;
for ( const row of getResources( "os" ) ) {
const match = row[ 0 ].exec( userAgent );
if ( match ) {
this.#family = row[ 1 ]?.replaceAll( /\$(\d+)/g, ( string, index ) => match[ index ] ) ?? match[ 1 ];
const major = row[ 2 ]?.replaceAll( /\$(\d+)/g, ( string, index ) => match[ index ] ) ?? match[ 2 ] ?? null,
minor = row[ 3 ]?.replaceAll( /\$(\d+)/g, ( string, index ) => match[ index ] ) ?? match[ 3 ] ?? null,
patch = row[ 4 ]?.replaceAll( /\$(\d+)/g, ( string, index ) => match[ index ] ) ?? match[ 4 ] ?? null,
build = row[ 5 ]?.replaceAll( /\$(\d+)/g, ( string, index ) => match[ index ] ) ?? match[ 5 ] ?? null;
this.#majorVersion = major == null
? null
: +major;
if ( major == null ) {
this.#majorVersion = null;
this.#version = null;
}
else {
this.#majorVersion = +major;
this.#version = major;
if ( minor != null ) {
this.#version += "." + minor;
if ( patch != null ) {
this.#version += "." + patch;
if ( build != null ) {
this.#version += "." + build;
}
}
}
}
break;
}
}
}
}
class UserAgentDevice extends UserAgentData {
#parsed;
#family = null;
#vendor = null;
#model = null;
#name;
// properties
get family () {
if ( !this.#parsed ) this.#parse();
return this.#family;
}
get vendor () {
if ( !this.#parsed ) this.#parse();
return this.#vendor;
}
get model () {
if ( !this.#parsed ) this.#parse();
return this.#model;
}
get name () {
if ( this.#name === undefined ) {
this.#name = [ this.vendor, this.model ].filter( tag => tag ).join( " " ) || null;
}
return this.#name;
}
// public
toString () {
return this.name;
}
toJSON () {
return {
"family": this.family,
"vendor": this.vendor,
"model": this.model,
};
}
// private
#parse () {
if ( this.#parsed ) return;
this.#parsed = true;
const userAgent = this.userAgent.userAgent;
if ( !userAgent ) return;
for ( const row of getResources( "device" ) ) {
const match = row[ 0 ].exec( userAgent );
if ( match ) {
this.#family = row[ 1 ]?.replaceAll( /\$(\d+)/g, ( string, index ) => match[ index ] ) ?? match[ 1 ] ?? null;
this.#vendor = row[ 2 ]?.replaceAll( /\$(\d+)/g, ( string, index ) => match[ index ] ) ?? match[ 2 ] ?? null;
this.#model = row[ 3 ]?.replaceAll( /\$(\d+)/g, ( string, index ) => match[ index ] ) ?? match[ 3 ] ?? null;
break;
}
}
}
}
export default class UserAgent {
#userAgent;
#reducedUserAgent;
#isChromium;
#chromiumBrand;
#platform;
#browser;
#os;
#device;
constructor ( userAgent ) {
this.#userAgent = userAgent;
}
// static
static new ( userAgent ) {
if ( userAgent instanceof this ) return userAgent;
var ua = CACHE.get( userAgent );
if ( !ua ) {
ua = new this( userAgent );
CACHE.set( userAgent, ua );
}
return ua;
}
static get chromiumBrands () {
return CHROMIUM_BRANDS;
}
static get platforms () {
return PLATFORMS;
}
// https://developers.google.com/privacy-sandbox/blog/user-agent-reduction-android-model-and-version
static createChromiumUserAgentString ( brand, platform, userAgentVersion ) {
const majorVersion = userAgentVersion.toString().split( "." )[ 0 ];
// resolve brand
brand = BROWSER_FAMILY_BRANDS[ brand.toLowerCase() ];
if ( !brand ) throw new Error( "Chromium brand is not valid" );
// resolve platform
platform = OS_FAMILY_PLATFORM[ platform.toLowerCase() ];
if ( !platform?.chromium ) throw new Error( "Chromium platform is not valid" );
// chromium
if ( brand.id === "chromium" ) {
return `Mozilla/5.0 (${ platform.chromium }) AppleWebKit/537.36 (KHTML, like Gecko) Chromium/${ majorVersion }.0.0.0${ platform.mobile
? " Mobile"
: "" } Safari/537.36`;
}
// chrome
else if ( brand.id === "chrome" ) {
return `Mozilla/5.0 (${ platform.chromium }) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${ majorVersion }.0.0.0${ platform.mobile
? " Mobile"
: "" } Safari/537.36`;
}
// edge
else if ( brand.id === "msedge" ) {
// edge android
if ( platform.id === "android" ) {
return `Mozilla/5.0 (${ platform.chromium }) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${ majorVersion }.0.0.0${ platform.mobile
? " Mobile"
: "" } Safari/537.36 EdgA/${ majorVersion }.0.0.0`;
}
// edge desktop
else {
return `Mozilla/5.0 (${ platform.chromium }) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${ majorVersion }.0.0.0${ platform.mobile
? " Mobile"
: "" } Safari/537.36 Edg/${ majorVersion }.0.0.0`;
}
}
}
// https://source.chromium.org/chromium/chromium/src/+/master:components/embedder_support/user_agent_utils.cc;l=55-100
static createChromiumBrands ( brands, userAgentVersion, useFullVersion ) {
if ( !Array.isArray( brands ) ) brands = [ brands ];
brands = new Set( brands.map( brand => BROWSER_FAMILY_BRANDS[ brand?.toLowerCase() ]?.fullName ).filter( brand => brand ) );
if ( !brands.size || brands.size > 3 ) return [];
brands.add( "Chromium" );
const seed = Number( userAgentVersion.split( "." )[ 0 ] ),
version = useFullVersion
? userAgentVersion
: seed.toString(),
order = CHROMIUM_BRANDS_ORDER[ brands.size ][ seed % CHROMIUM_BRANDS_ORDER[ brands.size ].length ],
greaseyChars = [ " ", "(", ":", "-", ".", "/", ")", ";", "=", "?", "_" ],
greasedVersions = [ "8", "99", "24" ],
greasedBrandVersionList = [];
brands.delete( "Chromium" );
// add "Not A Brand"
greasedBrandVersionList[ order[ 0 ] ] = {
"brand": `Not${ greaseyChars[ seed % greaseyChars.length ] }A${ greaseyChars[ ( seed + 1 ) % greaseyChars.length ] }Brand`,
"version": greasedVersions[ seed % greasedVersions.length ],
};
// add "Chromium" brand
greasedBrandVersionList[ order[ 1 ] ] = {
"brand": "Chromium",
version,
};
// add other brands
let n = 1;
for ( const brand of brands ) {
n += 1;
greasedBrandVersionList[ order[ n ] ] = {
brand,
version,
};
}
return greasedBrandVersionList;
}
// properties
get userAgent () {
return this.#userAgent;
}
get reducedUserAgent () {
if ( this.#reducedUserAgent == null ) {
if ( this.isChromium ) {
this.#reducedUserAgent = this.constructor.createChromiumUserAgentString( this.browser.family, this.os.family, this.browser.version );
}
else {
this.#reducedUserAgent = this.userAgent;
}
}
return this.#reducedUserAgent;
}
get isChromium () {
this.#isChromium ??= /chrom(?:e|ium)/i.test( this.userAgent.userAgent );
return this.#isChromium;
}
get chromiumBrand () {
if ( this.#chromiumBrand === undefined ) {
this.#chromiumBrand = BROWSER_FAMILY_BRANDS[ this.browser.family?.toLowerCase() ] || null;
}
return this.#chromiumBrand;
}
get platform () {
if ( this.#platform === undefined ) {
this.#platform = OS_FAMILY_PLATFORM[ this.os.family?.toLowerCase() ] || null;
}
return this.#platform;
}
get webgl () {
return this.platform?.webgl || WEBGL_DEFAULT;
}
get browser () {
this.#browser ??= new UserAgentBrowser( this );
return this.#browser;
}
get os () {
this.#os ??= new UserAgentOs( this );
return this.#os;
}
get device () {
this.#device ??= new UserAgentDevice( this );
return this.#device;
}
// public
toString () {
return this.userAgent;
}
toJSON () {
return {
"userAgent": this.userAgent,
"isChromium": this.isChromium,
"browser": this.browser.toJSON(),
"os": this.os.toJSON(),
"device": this.device.toJSON(),
};
}
createChromiumBrands ( useFullVersion ) {
return this.constructor.createChromiumBrands( this.browser.family, this.browser.version, useFullVersion );
}
}