rebrowser-playwright-core
Version:
A drop-in replacement for playwright-core patched with rebrowser-patches. It allows to pass modern automation detection tests.
338 lines (336 loc) • 16.3 kB
JavaScript
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.kNoXServerRunningError = exports.BrowserType = exports.BrowserReadyState = void 0;
var _fs = _interopRequireDefault(require("fs"));
var os = _interopRequireWildcard(require("os"));
var _path = _interopRequireDefault(require("path"));
var _browserContext = require("./browserContext");
var _registry = require("./registry");
var _transport = require("./transport");
var _processLauncher = require("../utils/processLauncher");
var _pipeTransport = require("./pipeTransport");
var _progress = require("./progress");
var _timeoutSettings = require("../common/timeoutSettings");
var _utils = require("../utils");
var _fileUtils = require("../utils/fileUtils");
var _helper = require("./helper");
var _debugLogger = require("../utils/debugLogger");
var _instrumentation = require("./instrumentation");
var _protocolError = require("./protocolError");
var _socksClientCertificatesInterceptor = require("./socksClientCertificatesInterceptor");
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const kNoXServerRunningError = exports.kNoXServerRunningError = 'Looks like you launched a headed browser without having a XServer running.\n' + 'Set either \'headless: true\' or use \'xvfb-run <your-playwright-app>\' before running Playwright.\n\n<3 Playwright Team';
class BrowserReadyState {
constructor() {
this._wsEndpoint = new _utils.ManualPromise();
}
onBrowserExit() {
// Unblock launch when browser prematurely exits.
this._wsEndpoint.resolve(undefined);
}
async waitUntilReady() {
const wsEndpoint = await this._wsEndpoint;
return {
wsEndpoint
};
}
}
exports.BrowserReadyState = BrowserReadyState;
class BrowserType extends _instrumentation.SdkObject {
constructor(parent, browserName) {
super(parent, 'browser-type');
this._name = void 0;
this._useBidi = false;
this.attribution.browserType = this;
this._name = browserName;
}
executablePath() {
return _registry.registry.findExecutable(this._name).executablePath(this.attribution.playwright.options.sdkLanguage) || '';
}
name() {
return this._name;
}
async launch(metadata, options, protocolLogger) {
options = this._validateLaunchOptions(options);
if (this._useBidi) options.useWebSocket = true;
const controller = new _progress.ProgressController(metadata, this);
controller.setLogName('browser');
const browser = await controller.run(progress => {
const seleniumHubUrl = options.__testHookSeleniumRemoteURL || process.env.SELENIUM_REMOTE_URL;
if (seleniumHubUrl) return this._launchWithSeleniumHub(progress, seleniumHubUrl, options);
return this._innerLaunchWithRetries(progress, options, undefined, _helper.helper.debugProtocolLogger(protocolLogger)).catch(e => {
throw this._rewriteStartupLog(e);
});
}, _timeoutSettings.TimeoutSettings.launchTimeout(options));
return browser;
}
async launchPersistentContext(metadata, userDataDir, options) {
const launchOptions = this._validateLaunchOptions(options);
if (this._useBidi) launchOptions.useWebSocket = true;
const controller = new _progress.ProgressController(metadata, this);
controller.setLogName('browser');
const browser = await controller.run(async progress => {
var _options$clientCertif;
// Note: Any initial TLS requests will fail since we rely on the Page/Frames initialize which sets ignoreHTTPSErrors.
let clientCertificatesProxy;
if ((_options$clientCertif = options.clientCertificates) !== null && _options$clientCertif !== void 0 && _options$clientCertif.length) {
var _clientCertificatesPr;
clientCertificatesProxy = new _socksClientCertificatesInterceptor.ClientCertificatesProxy(options);
launchOptions.proxyOverride = await ((_clientCertificatesPr = clientCertificatesProxy) === null || _clientCertificatesPr === void 0 ? void 0 : _clientCertificatesPr.listen());
options = {
...options
};
options.internalIgnoreHTTPSErrors = true;
}
progress.cleanupWhenAborted(() => {
var _clientCertificatesPr2;
return (_clientCertificatesPr2 = clientCertificatesProxy) === null || _clientCertificatesPr2 === void 0 ? void 0 : _clientCertificatesPr2.close();
});
const browser = await this._innerLaunchWithRetries(progress, launchOptions, options, _helper.helper.debugProtocolLogger(), userDataDir).catch(e => {
throw this._rewriteStartupLog(e);
});
browser._defaultContext._clientCertificatesProxy = clientCertificatesProxy;
return browser;
}, _timeoutSettings.TimeoutSettings.launchTimeout(launchOptions));
return browser._defaultContext;
}
async _innerLaunchWithRetries(progress, options, persistent, protocolLogger, userDataDir) {
try {
return await this._innerLaunch(progress, options, persistent, protocolLogger, userDataDir);
} catch (error) {
// @see https://github.com/microsoft/playwright/issues/5214
const errorMessage = typeof error === 'object' && typeof error.message === 'string' ? error.message : '';
if (errorMessage.includes('Inconsistency detected by ld.so')) {
progress.log(`<restarting browser due to hitting race condition in glibc>`);
return this._innerLaunch(progress, options, persistent, protocolLogger, userDataDir);
}
throw error;
}
}
async _innerLaunch(progress, options, persistent, protocolLogger, maybeUserDataDir) {
options.proxy = options.proxy ? (0, _browserContext.normalizeProxySettings)(options.proxy) : undefined;
const browserLogsCollector = new _debugLogger.RecentLogsCollector();
const {
browserProcess,
userDataDir,
artifactsDir,
transport
} = await this._launchProcess(progress, options, !!persistent, browserLogsCollector, maybeUserDataDir);
if (options.__testHookBeforeCreateBrowser) await options.__testHookBeforeCreateBrowser();
const browserOptions = {
name: this._name,
isChromium: this._name === 'chromium',
channel: options.channel,
slowMo: options.slowMo,
persistent,
headful: !options.headless,
artifactsDir,
downloadsPath: options.downloadsPath || artifactsDir,
tracesDir: options.tracesDir || artifactsDir,
browserProcess,
customExecutablePath: options.executablePath,
proxy: options.proxy,
protocolLogger,
browserLogsCollector,
wsEndpoint: options.useWebSocket ? transport.wsEndpoint : undefined,
originalLaunchOptions: options
};
if (persistent) (0, _browserContext.validateBrowserContextOptions)(persistent, browserOptions);
copyTestHooks(options, browserOptions);
const browser = await this.connectToTransport(transport, browserOptions);
browser._userDataDirForTest = userDataDir;
// We assume no control when using custom arguments, and do not prepare the default context in that case.
if (persistent && !options.ignoreAllDefaultArgs) await browser._defaultContext._loadDefaultContext(progress);
return browser;
}
async _launchProcess(progress, options, isPersistent, browserLogsCollector, userDataDir) {
var _await$readyState$wai;
const {
ignoreDefaultArgs,
ignoreAllDefaultArgs,
args = [],
executablePath = null,
handleSIGINT = true,
handleSIGTERM = true,
handleSIGHUP = true
} = options;
const env = options.env ? (0, _processLauncher.envArrayToObject)(options.env) : process.env;
await this._createArtifactDirs(options);
const tempDirectories = [];
const artifactsDir = await _fs.default.promises.mkdtemp(_path.default.join(os.tmpdir(), 'playwright-artifacts-'));
tempDirectories.push(artifactsDir);
if (userDataDir) {
// Firefox bails if the profile directory does not exist, Chrome creates it. We ensure consistent behavior here.
if (!(await (0, _fileUtils.existsAsync)(userDataDir))) await _fs.default.promises.mkdir(userDataDir, {
recursive: true,
mode: 0o700
});
} else {
userDataDir = await _fs.default.promises.mkdtemp(_path.default.join(os.tmpdir(), `playwright_${this._name}dev_profile-`));
tempDirectories.push(userDataDir);
}
await this.prepareUserDataDir(options, userDataDir);
const browserArguments = [];
if (ignoreAllDefaultArgs) browserArguments.push(...args);else if (ignoreDefaultArgs) browserArguments.push(...this.defaultArgs(options, isPersistent, userDataDir).filter(arg => ignoreDefaultArgs.indexOf(arg) === -1));else browserArguments.push(...this.defaultArgs(options, isPersistent, userDataDir));
let executable;
if (executablePath) {
if (!(await (0, _fileUtils.existsAsync)(executablePath))) throw new Error(`Failed to launch ${this._name} because executable doesn't exist at ${executablePath}`);
executable = executablePath;
} else {
const registryExecutable = _registry.registry.findExecutable(this.getExecutableName(options));
if (!registryExecutable || registryExecutable.browserName !== this._name) throw new Error(`Unsupported ${this._name} channel "${options.channel}"`);
executable = registryExecutable.executablePathOrDie(this.attribution.playwright.options.sdkLanguage);
await _registry.registry.validateHostRequirementsForExecutablesIfNeeded([registryExecutable], this.attribution.playwright.options.sdkLanguage);
}
const readyState = this.readyState(options);
// Note: it is important to define these variables before launchProcess, so that we don't get
// "Cannot access 'browserServer' before initialization" if something went wrong.
let transport = undefined;
let browserProcess = undefined;
const {
launchedProcess,
gracefullyClose,
kill
} = await (0, _processLauncher.launchProcess)({
command: executable,
args: browserArguments,
env: this.amendEnvironment(env, userDataDir, executable, browserArguments),
handleSIGINT,
handleSIGTERM,
handleSIGHUP,
log: message => {
readyState === null || readyState === void 0 || readyState.onBrowserOutput(message);
progress.log(message);
browserLogsCollector.log(message);
},
stdio: 'pipe',
tempDirectories,
attemptToGracefullyClose: async () => {
if (options.__testHookGracefullyClose) await options.__testHookGracefullyClose();
// We try to gracefully close to prevent crash reporting and core dumps.
// Note that it's fine to reuse the pipe transport, since
// our connection ignores kBrowserCloseMessageId.
this.attemptToGracefullyCloseBrowser(transport);
},
onExit: (exitCode, signal) => {
// Unblock launch when browser prematurely exits.
readyState === null || readyState === void 0 || readyState.onBrowserExit();
if (browserProcess && browserProcess.onclose) browserProcess.onclose(exitCode, signal);
}
});
async function closeOrKill(timeout) {
let timer;
try {
await Promise.race([gracefullyClose(), new Promise((resolve, reject) => timer = setTimeout(reject, timeout))]);
} catch (ignored) {
await kill().catch(ignored => {}); // Make sure to await actual process exit.
} finally {
clearTimeout(timer);
}
}
browserProcess = {
onclose: undefined,
process: launchedProcess,
close: () => closeOrKill(options.__testHookBrowserCloseTimeout || _timeoutSettings.DEFAULT_TIMEOUT),
kill
};
progress.cleanupWhenAborted(() => closeOrKill(progress.timeUntilDeadline()));
const wsEndpoint = (_await$readyState$wai = await (readyState === null || readyState === void 0 ? void 0 : readyState.waitUntilReady())) === null || _await$readyState$wai === void 0 ? void 0 : _await$readyState$wai.wsEndpoint;
if (options.useWebSocket) {
transport = await _transport.WebSocketTransport.connect(progress, wsEndpoint);
} else {
const stdio = launchedProcess.stdio;
transport = new _pipeTransport.PipeTransport(stdio[3], stdio[4]);
}
return {
browserProcess,
artifactsDir,
userDataDir,
transport
};
}
async _createArtifactDirs(options) {
if (options.downloadsPath) await _fs.default.promises.mkdir(options.downloadsPath, {
recursive: true
});
if (options.tracesDir) await _fs.default.promises.mkdir(options.tracesDir, {
recursive: true
});
}
async connectOverCDP(metadata, endpointURL, options, timeout) {
throw new Error('CDP connections are only supported by Chromium');
}
async _launchWithSeleniumHub(progress, hubUrl, options) {
throw new Error('Connecting to SELENIUM_REMOTE_URL is only supported by Chromium');
}
_validateLaunchOptions(options) {
const {
devtools = false
} = options;
let {
headless = !devtools,
downloadsPath,
proxy
} = options;
if ((0, _utils.debugMode)()) headless = false;
if (downloadsPath && !_path.default.isAbsolute(downloadsPath)) downloadsPath = _path.default.join(process.cwd(), downloadsPath);
if (this.attribution.playwright.options.socksProxyPort) proxy = {
server: `socks5://127.0.0.1:${this.attribution.playwright.options.socksProxyPort}`
};
return {
...options,
devtools,
headless,
downloadsPath,
proxy
};
}
_createUserDataDirArgMisuseError(userDataDirArg) {
switch (this.attribution.playwright.options.sdkLanguage) {
case 'java':
return new Error(`Pass userDataDir parameter to 'BrowserType.launchPersistentContext(userDataDir, options)' instead of specifying '${userDataDirArg}' argument`);
case 'python':
return new Error(`Pass user_data_dir parameter to 'browser_type.launch_persistent_context(user_data_dir, **kwargs)' instead of specifying '${userDataDirArg}' argument`);
case 'csharp':
return new Error(`Pass userDataDir parameter to 'BrowserType.LaunchPersistentContextAsync(userDataDir, options)' instead of specifying '${userDataDirArg}' argument`);
default:
return new Error(`Pass userDataDir parameter to 'browserType.launchPersistentContext(userDataDir, options)' instead of specifying '${userDataDirArg}' argument`);
}
}
_rewriteStartupLog(error) {
if (!(0, _protocolError.isProtocolError)(error)) return error;
return this.doRewriteStartupLog(error);
}
readyState(options) {
return undefined;
}
async prepareUserDataDir(options, userDataDir) {}
getExecutableName(options) {
return options.channel || this._name;
}
}
exports.BrowserType = BrowserType;
function copyTestHooks(from, to) {
for (const [key, value] of Object.entries(from)) {
if (key.startsWith('__testHook')) to[key] = value;
}
}
;