mirror of
https://github.com/Llewellynvdm/nativefier.git
synced 2025-01-22 22:58:33 +00:00
Improve user agent handling/provide user agent "short" codes (#1198)
This commit is contained in:
parent
4f3b449218
commit
d6730f7022
75
API.md
75
API.md
@ -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]
|
||||
|
@ -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
|
||||
```
|
||||
|
@ -20,6 +20,6 @@
|
||||
"source-map-support": "^0.5.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^12.0.1"
|
||||
"electron": "^12.0.7"
|
||||
}
|
||||
}
|
||||
|
@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -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} `, ' ');
|
||||
}
|
||||
|
@ -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];
|
||||
|
@ -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,
|
||||
|
@ -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', {
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
58
src/infer/browsers/inferChromeVersion.ts
Normal file
58
src/infer/browsers/inferChromeVersion.ts
Normal 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;
|
||||
}
|
||||
}
|
49
src/infer/browsers/inferFirefoxVersion.ts
Normal file
49
src/infer/browsers/inferFirefoxVersion.ts
Normal 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;
|
||||
}
|
||||
}
|
74
src/infer/browsers/inferSafariVersion.ts
Normal file
74
src/infer/browsers/inferSafariVersion.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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',
|
||||
);
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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');
|
||||
});
|
||||
|
@ -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');
|
||||
});
|
||||
|
@ -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}`;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
);
|
||||
});
|
||||
|
@ -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();
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user