Improve user agent handling/provide user agent "short" codes (#1198)

This commit is contained in:
Adam Weeden 2021-05-21 23:41:13 -04:00 committed by GitHub
parent 4f3b449218
commit d6730f7022
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 526 additions and 228 deletions

75
API.md
View File

@ -52,10 +52,13 @@
- [[zoom]](#zoom)
- [Internal Browser Options](#internal-browser-options)
- [[file-download-options]](#file-download-options)
- [[honest]](#honest)
- [[inject]](#inject)
- [[lang]](#lang)
- [[user-agent]](#user-agent)
- [[user-agent-honest]](#user-agent-honest)
- [Internal Browser Cache Options](#internal-browser-cache-options)
- [[clear-cache]](#clear-cache)
- [[disk-cache-size]](#disk-cache-size)
- [URL Handling Options](#url-handling-options)
- [[block-external-urls]](#block-external-urls)
- [[internal-urls]](#internal-urls)
@ -66,9 +69,6 @@
- [[disable-gpu]](#disable-gpu)
- [[enable-es3-apis]](#enable-es3-apis)
- [[ignore-gpu-blacklist]](#ignore-gpu-blacklist)
- [Caching Options](#caching-options)
- [[clear-cache]](#clear-cache)
- [[disk-cache-size]](#disk-cache-size)
- [(In)Security Options](#in-security-options)
- [[disable-old-build-warning-yesiknowitisinsecure]](#disable-old-build-warning-yesiknowitisinsecure)
- [[ignore-certificate]](#ignore-certificate)
@ -672,16 +672,6 @@ Example `shortcuts.json` for `https://deezer.com` & `https://soundcloud.com` to
On MacOS 10.14+, if you have set a global shortcut that includes a Media key, the user will need to be prompted for permissions to enable these keys in System Preferences > Security & Privacy > Accessibility.
#### [honest]
```
--honest
```
By default, Nativefier uses a preset user agent string for your OS and masquerades as a regular Google Chrome browser, so that sites like WhatsApp Web will not say that the current browser is unsupported.
If this flag is passed, it will not override the user agent.
#### [inject]
```
@ -712,7 +702,41 @@ Set the language or locale to render the web site as (e.g., "fr", "en-US", "es",
-u, --user-agent <value>
```
Set the user agent to run the created app with.
Set the user agent to run the created app with. Use `--user-agent-honest` to use the true Electron user agent.
The following short codes are also supported to generate a user agent: `edge`, `firefox`, `safari`.
- `edge` will generate a Microsoft Edge user agent matching the Chrome version of Electron being used
- `firefox` will generate a Mozilla Firefox user agent matching the latest stable release of that browser
- `safari` will generate an Apple Safari user agent matching the latest stable release of that browser
#### [user-agent-honest]
```
--user-agent-honest, --honest
```
By default, Nativefier uses a preset user agent string for your OS and masquerades as a regular Google Chrome browser, so that for some sites, it will not say that the current browser is unsupported.
If this flag is passed, it will not override the user agent, and use Electron's default generated one for your app.
### Internal Browser Cache Options
#### [clear-cache]
```
--clear-cache
```
Prevents the application from preserving cache between launches.
#### [disk-cache-size]
```
--disk-cache-size <value>
```
Forces the maximum disk space to be used by the disk cache. Value is given in bytes.
### URL Handling Options
@ -818,27 +842,6 @@ Passes the enable-es3-apis flag to the Chrome engine, to force the activation of
Passes the ignore-gpu-blacklist flag to the Chrome engine, to allow for WebGl apps to work on non supported graphics cards.
### Caching Options
#### [clear-cache]
```
--clear-cache
```
Prevents the application from preserving cache between launches.
#### [disk-cache-size]
```
--disk-cache-size <value>
```
Forces the maximum disk space to be used by the disk cache. Value is given in bytes.
### (In)Security Options
#### [ignore-certificate]

View File

@ -15,7 +15,7 @@ Below you'll find a list of build commands contributed by the Nativefier communi
```sh
nativefier 'https://docs.google.com/spreadsheets' \
--user-agent 'user agent of current stable Firefox'
--user-agent firefox
```
Note: lying about the User Agent is required, else Google will notice your "Chrome" isn't a real Chrome, and will refuse access.
@ -58,7 +58,7 @@ Note: as for Udemy, `--widevine` + [app signing](https://github.com/nativefier/n
```sh
nativefier 'https://open.spotify.com/'
--widevine
-u 'useragent of a non-Chrome browser, e.g. the current stable Firefox'
--user-agent firefox
--inject spotify.js
--inject spotify.css
```

View File

@ -20,6 +20,6 @@
"source-map-support": "^0.5.19"
},
"devDependencies": {
"electron": "^12.0.1"
"electron": "^12.0.7"
}
}

View File

@ -1,4 +1,8 @@
import { linkIsInternal, getCounterValue } from './helpers';
import {
linkIsInternal,
getCounterValue,
removeUserAgentSpecifics,
} from './helpers';
const internalUrl = 'https://medium.com/';
const internalUrlWww = 'https://www.medium.com/';
@ -146,3 +150,19 @@ test('getCounterValue should return a string for small counter numbers in the ti
test('getCounterValue should return a string for large counter numbers in the title', () => {
expect(getCounterValue(largeCounterTitle)).toEqual('8,756');
});
describe('removeUserAgentSpecifics', () => {
test('removes Electron and App specific info', () => {
const userAgentFallback =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) app-nativefier-804458/1.0.0 Chrome/89.0.4389.128 Electron/12.0.7 Safari/537.36';
expect(
removeUserAgentSpecifics(
userAgentFallback,
'app-nativefier-804458',
'1.0.0',
),
).not.toBe(
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.128 Safari/537.36',
);
});
});

View File

@ -160,3 +160,18 @@ export function getCounterValue(title: string): string {
const match = itemCountRegex.exec(title);
return match ? match[1] : undefined;
}
export function removeUserAgentSpecifics(
userAgentFallback: string,
appName: string,
appVersion: string,
): string {
// Electron userAgentFallback is the user agent used if none is specified when creating a window.
// For our purposes, it's useful because its format is similar enough to a real Chrome's user agent to not need
// to infer the userAgent. userAgentFallback normally looks like this:
// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) app-nativefier-804458/1.0.0 Chrome/89.0.4389.128 Electron/12.0.7 Safari/537.36
// We just need to strip out the appName/1.0.0 and Electron/electronVersion
return userAgentFallback
.replace(`Electron/${process.versions.electron} `, '')
.replace(`${appName}/${appVersion} `, ' ');
}

View File

@ -21,7 +21,7 @@ import {
APP_ARGS_FILE_PATH,
} from './components/mainWindow';
import { createTrayIcon } from './components/trayIcon';
import { isOSX } from './helpers/helpers';
import { isOSX, removeUserAgentSpecifics } from './helpers/helpers';
import { inferFlashPath } from './helpers/inferFlash';
// Entrypoint for Squirrel, a windows update framework. See https://github.com/nativefier/nativefier/pull/744
@ -46,6 +46,14 @@ if (appArgs.portable) {
app.setPath('userData', path.join(__dirname, '..', 'appData'));
}
if (!appArgs.userAgentHonest) {
app.userAgentFallback = removeUserAgentSpecifics(
app.userAgentFallback,
app.getName(),
app.getVersion(),
);
}
// Take in a URL on the command line as an override
if (process.argv.length > 1) {
const maybeUrl = process.argv[1];

View File

@ -53,7 +53,6 @@ function pickElectronAppArgs(options: AppOptions): any {
height: options.nativefier.height,
helperBundleId: options.packager.helperBundleId,
hideWindowFrame: options.nativefier.hideWindowFrame,
honest: options.nativefier.honest,
ignoreCertificate: options.nativefier.ignoreCertificate,
ignoreGpuBlacklist: options.nativefier.ignoreGpuBlacklist,
insecure: options.nativefier.insecure,
@ -83,7 +82,7 @@ function pickElectronAppArgs(options: AppOptions): any {
tray: options.nativefier.tray,
usageDescription: options.packager.usageDescription,
userAgent: options.nativefier.userAgent,
userAgentOverriden: options.nativefier.userAgentOverriden,
userAgentHonest: options.nativefier.userAgentHonest,
versionString: options.nativefier.versionString,
width: options.nativefier.width,
widevine: options.nativefier.widevine,

View File

@ -300,7 +300,8 @@ export function initArgs(argv: string[]): yargs.Argv<any> {
})
.option('u', {
alias: 'user-agent',
description: "set the app's user agent string",
description:
"set the app's user agent string; may also use 'edge', 'firefox', or 'safari' to have one auto-generated",
type: 'string',
})
.option('user-agent-honest', {

View File

@ -2,10 +2,22 @@ import * as path from 'path';
export const DEFAULT_APP_NAME = 'APP';
// Update both together, and update app / package.json / devDeps / electron
// Update both DEFAULT_ELECTRON_VERSION and DEFAULT_CHROME_VERSION together,
// and update app / package.json / devDeps / electron to value of DEFAULT_ELECTRON_VERSION
export const DEFAULT_ELECTRON_VERSION = '12.0.7';
export const DEFAULT_CHROME_VERSION = '89.0.4389.128';
// Update each of these periodically
// https://product-details.mozilla.org/1.0/firefox_versions.json
export const DEFAULT_FIREFOX_VERSION = '88.0.1';
// https://en.wikipedia.org/wiki/Safari_version_history
export const DEFAULT_SAFARI_VERSION = {
majorVersion: 14,
version: '14.0.3',
webkitVersion: '610.4.3.1.7',
};
export const ELECTRON_MAJOR_VERSION = parseInt(
DEFAULT_ELECTRON_VERSION.split('.')[0],
10,

View File

@ -184,13 +184,6 @@ export function useOldAppOptions(
log.debug('rawOptions', rawOptions);
log.debug('oldApp', oldApp);
if (
oldApp.options.userAgentOverriden === undefined ||
oldApp.options.userAgentOverriden === false
) {
oldApp.options.userAgent = undefined;
}
const combinedOptions = { ...rawOptions, ...oldApp.options };
log.debug('Combined options', combinedOptions);

View File

@ -0,0 +1,58 @@
import axios from 'axios';
import * as log from 'loglevel';
import {
DEFAULT_CHROME_VERSION,
DEFAULT_ELECTRON_VERSION,
} from '../../constants';
type ElectronRelease = {
version: string;
date: string;
node: string;
v8: string;
uv: string;
zlib: string;
openssl: string;
modules: string;
chrome: string;
files: string[];
};
const ELECTRON_VERSIONS_URL = 'https://atom.io/download/atom-shell/index.json';
export async function getChromeVersionForElectronVersion(
electronVersion: string,
url = ELECTRON_VERSIONS_URL,
): Promise<string> {
if (!electronVersion || electronVersion === DEFAULT_ELECTRON_VERSION) {
// Exit quickly for the scenario that we already know about
return DEFAULT_CHROME_VERSION;
}
try {
log.debug('Grabbing electron<->chrome versions file from', url);
const response = await axios.get(url, { timeout: 5000 });
if (response.status !== 200) {
throw new Error(`Bad request: Status code ${response.status}`);
}
const electronReleases: ElectronRelease[] = response.data;
const electronVersionToChromeVersion: { [key: string]: string } = {};
for (const release of electronReleases) {
electronVersionToChromeVersion[release.version] = release.chrome;
}
if (!(electronVersion in electronVersionToChromeVersion)) {
throw new Error(
`Electron version '${electronVersion}' not found in retrieved version list!`,
);
}
const chromeVersion = electronVersionToChromeVersion[electronVersion];
log.debug(
`Associated electron v${electronVersion} to chrome v${chromeVersion}`,
);
return chromeVersion;
} catch (err) {
log.error('getChromeVersionForElectronVersion ERROR', err);
log.debug('Falling back to default Chrome version', DEFAULT_CHROME_VERSION);
return DEFAULT_CHROME_VERSION;
}
}

View File

@ -0,0 +1,49 @@
import axios from 'axios';
import * as log from 'loglevel';
import { DEFAULT_FIREFOX_VERSION } from '../../constants';
type FirefoxVersions = {
FIREFOX_AURORA: string;
FIREFOX_DEVEDITION: string;
FIREFOX_ESR: string;
FIREFOX_ESR_NEXT: string;
FIREFOX_NIGHTLY: string;
LAST_MERGE_DATE: string;
LAST_RELEASE_DATE: string;
LAST_SOFTFREEZE_DATE: string;
LATEST_FIREFOX_DEVEL_VERSION: string;
LATEST_FIREFOX_OLDER_VERSION: string;
LATEST_FIREFOX_RELEASED_DEVEL_VERSION: string;
LATEST_FIREFOX_VERSION: string;
NEXT_MERGE_DATE: string;
NEXT_RELEASE_DATE: string;
NEXT_SOFTFREEZE_DATE: string;
};
const FIREFOX_VERSIONS_URL =
'https://product-details.mozilla.org/1.0/firefox_versions.json';
export async function getLatestFirefoxVersion(
url = FIREFOX_VERSIONS_URL,
): Promise<string> {
try {
log.debug('Grabbing Firefox version data from', url);
const response = await axios.get(url, { timeout: 5000 });
if (response.status !== 200) {
throw new Error(`Bad request: Status code ${response.status}`);
}
const firefoxVersions: FirefoxVersions = response.data;
log.debug(
`Got latest Firefox version ${firefoxVersions.LATEST_FIREFOX_VERSION}`,
);
return firefoxVersions.LATEST_FIREFOX_VERSION;
} catch (err) {
log.error('getLatestFirefoxVersion ERROR', err);
log.debug(
'Falling back to default Firefox version',
DEFAULT_FIREFOX_VERSION,
);
return DEFAULT_FIREFOX_VERSION;
}
}

View File

@ -0,0 +1,74 @@
import axios from 'axios';
import * as log from 'loglevel';
import { DEFAULT_SAFARI_VERSION } from '../../constants';
export type SafariVersion = {
majorVersion: number;
version: string;
webkitVersion: string;
};
const SAFARI_VERSIONS_HISTORY_URL =
'https://en.wikipedia.org/wiki/Safari_version_history';
export async function getLatestSafariVersion(
url = SAFARI_VERSIONS_HISTORY_URL,
): Promise<SafariVersion> {
try {
log.debug('Grabbing apple version data from', url);
const response = await axios.get(url, { timeout: 5000 });
if (response.status !== 200) {
throw new Error(`Bad request: Status code ${response.status}`);
}
// This would be easier with an HTML parser, but we're not going to include an extra dependency for something that dumb
const rawData: string = response.data;
const majorVersions = [
...rawData.matchAll(
/class="mw-headline" id="Safari_[0-9]*">Safari ([0-9]*)</g,
),
].map((match) => match[1]);
const majorVersion = parseInt(majorVersions[majorVersions.length - 1]);
const majorVersionTable = rawData
.split('>Release history<')[2]
.split('<table')
.filter((table) => table.includes(`Safari ${majorVersion}.x`))[0];
const versionRows = majorVersionTable.split('<tbody')[1].split('<tr');
let version = null;
let webkitVersion: string = null;
for (const versionRow of versionRows.reverse()) {
const versionMatch = [
...versionRow.matchAll(/>\s*(([0-9]*\.){2}[0-9])\s*</g),
];
if (versionMatch.length > 0 && !version) {
version = versionMatch[0][1];
}
const webkitVersionMatch = [
...versionRow.matchAll(/>\s*(([0-9]*\.){3,4}[0-9])\s*</g),
];
if (webkitVersionMatch.length > 0 && !webkitVersion) {
webkitVersion = webkitVersionMatch[0][1];
}
if (version && webkitVersion) {
break;
}
}
return {
majorVersion,
version,
webkitVersion,
};
} catch (err) {
log.error('getLatestSafariVersion ERROR', err);
log.debug('Falling back to default Safari version', DEFAULT_SAFARI_VERSION);
return DEFAULT_SAFARI_VERSION;
}
}

View File

@ -1,30 +0,0 @@
import { inferUserAgent } from './inferUserAgent';
import { DEFAULT_ELECTRON_VERSION, DEFAULT_CHROME_VERSION } from '../constants';
const EXPECTED_USERAGENTS = {
darwin: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${DEFAULT_CHROME_VERSION} Safari/537.36`,
mas: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${DEFAULT_CHROME_VERSION} Safari/537.36`,
win32: `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${DEFAULT_CHROME_VERSION} Safari/537.36`,
linux: `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${DEFAULT_CHROME_VERSION} Safari/537.36`,
};
describe('Infer User Agent', () => {
// TODO make fast by mocking timeout.
for (const [platform, archUa] of Object.entries(EXPECTED_USERAGENTS)) {
test(`Can infer userAgent for ${platform}`, async () => {
jest.setTimeout(10000);
const ua = await inferUserAgent(DEFAULT_ELECTRON_VERSION, platform);
expect(ua).toBe(archUa);
});
}
// TODO make fast by mocking timeout, and un-skip
test.skip('Connection error will still get a user agent', async () => {
jest.setTimeout(6000);
const TIMEOUT_URL = 'http://www.google.com:81/';
await expect(inferUserAgent('1.6.7', 'darwin', TIMEOUT_URL)).resolves.toBe(
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36',
);
});
});

View File

@ -1,94 +0,0 @@
import axios from 'axios';
import * as log from 'loglevel';
import { DEFAULT_CHROME_VERSION } from '../constants';
const ELECTRON_VERSIONS_URL = 'https://atom.io/download/atom-shell/index.json';
type ElectronRelease = {
version: string;
date: string;
node: string;
v8: string;
uv: string;
zlib: string;
openssl: string;
modules: string;
chrome: string;
files: string[];
};
async function getChromeVersionForElectronVersion(
electronVersion: string,
url = ELECTRON_VERSIONS_URL,
): Promise<string> {
log.debug('Grabbing electron<->chrome versions file from', url);
const response = await axios.get(url, { timeout: 5000 });
if (response.status !== 200) {
throw new Error(`Bad request: Status code ${response.status}`);
}
const electronReleases: ElectronRelease[] = response.data;
const electronVersionToChromeVersion: { [key: string]: string } = {};
for (const release of electronReleases) {
electronVersionToChromeVersion[release.version] = release.chrome;
}
if (!(electronVersion in electronVersionToChromeVersion)) {
throw new Error(
`Electron version '${electronVersion}' not found in retrieved version list!`,
);
}
const chromeVersion = electronVersionToChromeVersion[electronVersion];
log.debug(
`Associated electron v${electronVersion} to chrome v${chromeVersion}`,
);
return chromeVersion;
}
export function getUserAgentString(
chromeVersion: string,
platform: string,
): string {
let userAgent: string;
switch (platform) {
case 'darwin':
case 'mas':
userAgent = `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
break;
case 'win32':
userAgent = `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
break;
case 'linux':
userAgent = `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
break;
default:
throw new Error(
'Error invalid platform specified to getUserAgentString()',
);
}
log.debug(
`Given chrome ${chromeVersion} on ${platform},`,
`using user agent: ${userAgent}`,
);
return userAgent;
}
export async function inferUserAgent(
electronVersion: string,
platform: string,
url = ELECTRON_VERSIONS_URL,
): Promise<string> {
log.debug(
`Inferring user agent for electron ${electronVersion} / ${platform}`,
);
try {
const chromeVersion = await getChromeVersionForElectronVersion(
electronVersion,
url,
);
return getUserAgentString(chromeVersion, platform);
} catch (e) {
log.warn(
`Unable to infer chrome version for user agent, using ${DEFAULT_CHROME_VERSION}`,
);
return getUserAgentString(DEFAULT_CHROME_VERSION, platform);
}
}

View File

@ -4,9 +4,12 @@ import * as path from 'path';
import { DEFAULT_ELECTRON_VERSION } from './constants';
import { getTempDir } from './helpers/helpers';
import { getChromeVersionForElectronVersion } from './infer/browsers/inferChromeVersion';
import { getLatestFirefoxVersion } from './infer/browsers/inferFirefoxVersion';
import { getLatestSafariVersion } from './infer/browsers/inferSafariVersion';
import { inferArch } from './infer/inferOs';
import { inferUserAgent } from './infer/inferUserAgent';
import { buildNativefierApp } from './main';
import { userAgent } from './options/fields/userAgent';
async function checkApp(appRoot: string, inputOptions: any): Promise<void> {
const arch = (inputOptions.arch as string) || inferArch();
@ -66,14 +69,18 @@ async function checkApp(appRoot: string, inputOptions: any): Promise<void> {
);
// Test user agent
expect(nativefierConfig.userAgent).toBe(
inputOptions.userAgent !== undefined
? inputOptions.userAgent
: await inferUserAgent(
if (inputOptions.userAgent) {
const translatedUserAgent = await userAgent({
packager: {
platform: inputOptions.platform,
electronVersion:
inputOptions.electronVersion || DEFAULT_ELECTRON_VERSION,
inputOptions.platform,
),
);
},
nativefier: { userAgent: inputOptions.userAgent },
});
inputOptions.userAgent = translatedUserAgent || inputOptions.userAgent;
}
expect(nativefierConfig.userAgent).toBe(inputOptions.userAgent);
// Test lang
expect(nativefierConfig.lang).toBe(inputOptions.lang);
@ -87,10 +94,10 @@ describe('Nativefier', () => {
async (platform) => {
const tempDirectory = getTempDir('integtest');
const options = {
platform,
targetUrl: 'https://google.com/',
out: tempDirectory,
overwrite: true,
platform,
lang: 'en-US',
};
const appPath = await buildNativefierApp(options);
@ -104,7 +111,7 @@ describe('Nativefier upgrade', () => {
test.each([
{ platform: 'darwin', arch: 'x64' },
{ platform: 'linux', arch: 'arm64', userAgent: 'FIREFOX' },
{ platform: 'linux', arch: 'arm64', userAgent: 'FIREFOX 60' },
// Exhaustive integration testing here would be neat, but takes too long.
// -> For now, only testing a subset of platforms/archs
// { platform: 'win32', arch: 'x64' },
@ -134,14 +141,29 @@ describe('Nativefier upgrade', () => {
const upgradeAppPath = await buildNativefierApp(upgradeOptions);
options.electronVersion = DEFAULT_ELECTRON_VERSION;
options.userAgent =
baseAppOptions.userAgent !== undefined
? baseAppOptions.userAgent
: await inferUserAgent(
DEFAULT_ELECTRON_VERSION,
baseAppOptions.platform,
);
options.userAgent = baseAppOptions.userAgent;
await checkApp(upgradeAppPath, options);
},
);
});
describe('Browser version retrieval', () => {
test('get chrome version with electron version', async () => {
await expect(getChromeVersionForElectronVersion('12.0.0')).resolves.toBe(
'89.0.4389.69',
);
});
test('get latest firefox version', async () => {
const firefoxVersion = await getLatestFirefoxVersion();
const majorVersion = parseInt(firefoxVersion.split('.')[0]);
expect(majorVersion).toBeGreaterThanOrEqual(88);
});
test('get latest safari version', async () => {
const safariVersion = await getLatestSafariVersion();
expect(safariVersion.majorVersion).toBeGreaterThanOrEqual(14);
});
});

View File

@ -18,7 +18,7 @@ test('fully-defined async options are returned as-is', async () => {
expect(options.nativefier.userAgent).toEqual('random user agent');
});
test('user agent is inferred if not passed', async () => {
test('user agent is ignored if not provided', async () => {
const options = {
packager: {
icon: '/my/icon.png',
@ -32,5 +32,22 @@ test('user agent is inferred if not passed', async () => {
// @ts-ignore
await processOptions(options);
expect(options.nativefier.userAgent).toMatch(/Linux.*Chrome/);
expect(options.nativefier.userAgent).toBeUndefined();
});
test('user agent short code is populated', async () => {
const options = {
packager: {
icon: '/my/icon.png',
name: 'my beautiful app ',
targetUrl: 'https://myurl.com',
dir: '/tmp/myapp',
platform: 'linux',
},
nativefier: { userAgent: 'edge' },
};
// @ts-ignore
await processOptions(options);
expect(options.nativefier.userAgent).not.toBe('edge');
});

View File

@ -1,26 +1,90 @@
import { getChromeVersionForElectronVersion } from '../../infer/browsers/inferChromeVersion';
import { getLatestFirefoxVersion } from '../../infer/browsers/inferFirefoxVersion';
import { getLatestSafariVersion } from '../../infer/browsers/inferSafariVersion';
import { userAgent } from './userAgent';
import { inferUserAgent } from '../../infer/inferUserAgent';
jest.mock('./../../infer/inferUserAgent');
jest.mock('./../../infer/browsers/inferChromeVersion');
jest.mock('./../../infer/browsers/inferFirefoxVersion');
jest.mock('./../../infer/browsers/inferSafariVersion');
test('when a userAgent parameter is passed', async () => {
expect(inferUserAgent).toHaveBeenCalledTimes(0);
const params = {
packager: {},
nativefier: { userAgent: 'valid user agent' },
};
await expect(userAgent(params)).resolves.toBe(null);
await expect(userAgent(params)).resolves.toBeNull();
});
test('no userAgent parameter is passed', async () => {
const params = {
packager: { electronVersion: '123', platform: 'mac' },
packager: { platform: 'mac' },
nativefier: {},
};
await userAgent(params);
expect(inferUserAgent).toHaveBeenCalledWith(
params.packager.electronVersion,
params.packager.platform,
);
await expect(userAgent(params)).resolves.toBeNull();
});
test('edge userAgent parameter is passed', async () => {
(getChromeVersionForElectronVersion as jest.Mock).mockImplementationOnce(() =>
Promise.resolve('99.0.0'),
);
const params = {
packager: { platform: 'darwin' },
nativefier: { userAgent: 'edge' },
};
const parsedUserAgent = await userAgent(params);
expect(parsedUserAgent).not.toBe(params.nativefier.userAgent);
expect(parsedUserAgent).toContain('Edg/99.0.0');
});
test('firefox userAgent parameter is passed', async () => {
(getLatestFirefoxVersion as jest.Mock).mockImplementationOnce(() =>
Promise.resolve('100.0.0'),
);
const params = {
packager: { platform: 'win32' },
nativefier: { userAgent: 'firefox' },
};
const parsedUserAgent = await userAgent(params);
expect(parsedUserAgent).not.toBe(params.nativefier.userAgent);
expect(parsedUserAgent).toContain('Firefox/100.0.0');
});
test('safari userAgent parameter is passed', async () => {
(getLatestSafariVersion as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({
majorVersion: 101,
version: '101.0.0',
webkitVersion: '600.0.0.0',
}),
);
const params = {
packager: { platform: 'linux' },
nativefier: { userAgent: 'safari' },
};
const parsedUserAgent = await userAgent(params);
expect(parsedUserAgent).not.toBe(params.nativefier.userAgent);
expect(parsedUserAgent).toContain('Version/101.0.0 Safari');
});
test('short userAgent parameter is passed with an electronVersion', async () => {
(getChromeVersionForElectronVersion as jest.Mock).mockImplementationOnce(() =>
Promise.resolve('102.0.0'),
);
const params = {
packager: { electronVersion: '12.0.0', platform: 'darwin' },
nativefier: { userAgent: 'edge' },
};
const parsedUserAgent = await userAgent(params);
expect(parsedUserAgent).not.toBe(params.nativefier.userAgent);
expect(parsedUserAgent).toContain('102.0.0');
expect(getChromeVersionForElectronVersion).toHaveBeenCalledWith('12.0.0');
});

View File

@ -1,4 +1,9 @@
import { inferUserAgent } from '../../infer/inferUserAgent';
import * as log from 'loglevel';
import { getChromeVersionForElectronVersion } from '../../infer/browsers/inferChromeVersion';
import { getLatestFirefoxVersion } from '../../infer/browsers/inferFirefoxVersion';
import { getLatestSafariVersion } from '../../infer/browsers/inferSafariVersion';
import { normalizePlatform } from '../optionsMain';
type UserAgentOpts = {
packager: {
@ -10,13 +15,68 @@ type UserAgentOpts = {
};
};
const USER_AGENT_PLATFORM_MAPS = {
darwin: 'Macintosh; Intel Mac OS X 10_15_7',
linux: 'X11; Linux x86_64',
win32: 'Windows NT 10.0; Win64; x64',
};
const USER_AGENT_SHORT_CODE_MAPS = {
edge: edgeUserAgent,
firefox: firefoxUserAgent,
safari: safariUserAgent,
};
export async function userAgent(options: UserAgentOpts): Promise<string> {
if (options.nativefier.userAgent) {
if (!options.nativefier.userAgent) {
// No user agent got passed. Let's handle it with the app.userAgentFallback
return null;
}
return inferUserAgent(
options.packager.electronVersion,
options.packager.platform,
);
if (
!Object.keys(USER_AGENT_SHORT_CODE_MAPS).includes(
options.nativefier.userAgent.toLowerCase(),
)
) {
// Real user agent got passed. No need to translate it.
log.debug(
`${options.nativefier.userAgent.toLowerCase()} not found in`,
Object.keys(USER_AGENT_SHORT_CODE_MAPS),
);
return null;
}
options.packager.platform = normalizePlatform(options.packager.platform);
const userAgentPlatform =
USER_AGENT_PLATFORM_MAPS[
options.packager.platform === 'mas' ? 'darwin' : options.packager.platform
];
const mapFunction = USER_AGENT_SHORT_CODE_MAPS[options.nativefier.userAgent];
return await mapFunction(userAgentPlatform, options.packager.electronVersion);
}
async function edgeUserAgent(
platform: string,
electronVersion: string,
): Promise<string> {
const chromeVersion = await getChromeVersionForElectronVersion(
electronVersion,
);
return `Mozilla/5.0 (${platform}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36 Edg/${chromeVersion}`;
}
async function firefoxUserAgent(platform: string): Promise<string> {
const firefoxVersion = await getLatestFirefoxVersion();
return `Mozilla/5.0 (${platform}; rv:${firefoxVersion}) Gecko/20100101 Firefox/${firefoxVersion}`;
}
async function safariUserAgent(platform: string): Promise<string> {
const safariVersion = await getLatestSafariVersion();
return `Mozilla/5.0 (${platform}) AppleWebKit/${safariVersion.webkitVersion} (KHTML, like Gecko) Version/${safariVersion.version} Safari/${safariVersion.webkitVersion}`;
}

View File

@ -36,7 +36,6 @@ export interface AppOptions {
fullScreen: boolean;
globalShortcuts: any;
hideWindowFrame: boolean;
honest: boolean;
ignoreCertificate: boolean;
ignoreGpuBlacklist: boolean;
inject: string[];
@ -52,7 +51,7 @@ export interface AppOptions {
titleBarStyle: string;
tray: string | boolean;
userAgent: string;
userAgentOverriden: boolean;
userAgentHonest: boolean;
verbose: boolean;
versionString: string;
width: number;

View File

@ -1,5 +1,6 @@
import { getOptions } from './optionsMain';
import { getOptions, normalizePlatform } from './optionsMain';
import * as asyncConfig from './asyncConfig';
import { inferPlatform } from '../infer/inferOs';
const mockedAsyncConfig = { some: 'options' };
let asyncConfigMock: jasmine.Spy;
@ -38,3 +39,21 @@ test('it should set the accessibility prompt option to true by default', async (
);
expect(result.nativefier.accessibilityPrompt).toEqual(true);
});
test.each([
{ platform: 'darwin', expectedPlatform: 'darwin' },
{ platform: 'mAc', expectedPlatform: 'darwin' },
{ platform: 'osx', expectedPlatform: 'darwin' },
{ platform: 'liNux', expectedPlatform: 'linux' },
{ platform: 'mas', expectedPlatform: 'mas' },
{ platform: 'WIN32', expectedPlatform: 'win32' },
{ platform: 'windows', expectedPlatform: 'win32' },
{},
])('it should be able to normalize the platform %s', (platformOptions) => {
if (!platformOptions.expectedPlatform) {
platformOptions.expectedPlatform = inferPlatform();
}
expect(normalizePlatform(platformOptions.platform)).toBe(
platformOptions.expectedPlatform,
);
});

View File

@ -37,7 +37,7 @@ export async function getOptions(rawOptions: any): Promise<AppOptions> {
name: typeof rawOptions.name === 'string' ? rawOptions.name : '',
out: rawOptions.out || process.cwd(),
overwrite: rawOptions.overwrite,
platform: rawOptions.platform || inferPlatform(),
platform: rawOptions.platform,
portable: rawOptions.portable || false,
targetUrl:
rawOptions.targetUrl === undefined
@ -78,7 +78,6 @@ export async function getOptions(rawOptions: any): Promise<AppOptions> {
fullScreen: rawOptions.fullScreen || false,
globalShortcuts: null,
hideWindowFrame: rawOptions.hideWindowFrame,
honest: rawOptions.honest || false,
ignoreCertificate: rawOptions.ignoreCertificate || false,
ignoreGpuBlacklist: rawOptions.ignoreGpuBlacklist || false,
inject: rawOptions.inject || [],
@ -94,8 +93,7 @@ export async function getOptions(rawOptions: any): Promise<AppOptions> {
titleBarStyle: rawOptions.titleBarStyle || null,
tray: rawOptions.tray || false,
userAgent: rawOptions.userAgent,
userAgentOverriden:
rawOptions.userAgent !== undefined && rawOptions.userAgent !== null,
userAgentHonest: rawOptions.userAgentHonest || false,
verbose: rawOptions.verbose,
versionString: rawOptions.versionString,
width: rawOptions.width || 1280,
@ -174,18 +172,14 @@ export async function getOptions(rawOptions: any): Promise<AppOptions> {
options.nativefier.insecure = true;
}
if (options.nativefier.honest) {
if (options.nativefier.userAgentHonest && options.nativefier.userAgent) {
options.nativefier.userAgent = null;
log.warn(
`\nATTENTION: user-agent AND user-agent-honest/honest were provided. In this case, honesty wins. user-agent will be ignored`,
);
}
const platform = options.packager.platform.toLowerCase();
if (platform === 'windows') {
options.packager.platform = 'win32';
}
if (['osx', 'mac', 'macos'].includes(platform)) {
options.packager.platform = 'darwin';
}
options.packager.platform = normalizePlatform(options.packager.platform);
if (options.nativefier.width > options.nativefier.maxWidth) {
options.nativefier.width = options.nativefier.maxWidth;
@ -218,3 +212,18 @@ export async function getOptions(rawOptions: any): Promise<AppOptions> {
return options;
}
export function normalizePlatform(platform: string): string {
if (!platform) {
return inferPlatform();
}
if (platform.toLowerCase() === 'windows') {
return 'win32';
}
if (['osx', 'mac', 'macos'].includes(platform.toLowerCase())) {
return 'darwin';
}
return platform.toLowerCase();
}