diff --git a/packages/playwright-core/src/remote/playwrightServer.ts b/packages/playwright-core/src/remote/playwrightServer.ts index bd60b8815d..f907018bf0 100644 --- a/packages/playwright-core/src/remote/playwrightServer.ts +++ b/packages/playwright-core/src/remote/playwrightServer.ts @@ -27,7 +27,7 @@ import { ManualPromise } from '../utils/manualPromise'; import type { AndroidDevice } from '../server/android/android'; import type { SocksProxy } from '../common/socksProxy'; import { debugLogger } from '../common/debugLogger'; -import { createHttpServer } from '../utils'; +import { createHttpServer, userAgentVersionMatchesErrorMessage } from '../utils'; import { perMessageDeflate } from '../server/transport'; let lastConnectionId = 0; @@ -45,6 +45,7 @@ type ServerOptions = { export class PlaywrightServer { private _preLaunchedPlaywright: Playwright | undefined; private _wsServer: WebSocketServer | undefined; + private _server: http.Server | undefined; private _options: ServerOptions; constructor(options: ServerOptions) { @@ -69,6 +70,7 @@ export class PlaywrightServer { response.end('Running'); }); server.on('error', error => debugLogger.log('server', String(error))); + this._server = server; const wsEndpoint = await new Promise((resolve, reject) => { server.listen(port, () => { @@ -84,8 +86,7 @@ export class PlaywrightServer { debugLogger.log('server', 'Listening at ' + wsEndpoint); this._wsServer = new wsServer({ - server, - path: this._options.path, + noServer: true, perMessageDeflate, }); const browserSemaphore = new Semaphore(this._options.maxConnections); @@ -96,6 +97,23 @@ export class PlaywrightServer { headers.push(process.env.PWTEST_SERVER_WS_HEADERS!); }); } + server.on('upgrade', (request, socket, head) => { + const pathname = new URL('http://localhost' + request.url!).pathname; + if (pathname !== this._options.path) { + socket.write(`HTTP/${request.httpVersion} 400 Bad Request\r\n\r\n`); + socket.destroy(); + return; + } + + const uaError = userAgentVersionMatchesErrorMessage(request.headers['user-agent'] || ''); + if (uaError) { + socket.write(`HTTP/${request.httpVersion} 428 Precondition Required\r\n\r\n${uaError}`); + socket.destroy(); + return; + } + + this._wsServer?.handleUpgrade(request, socket, head, ws => this._wsServer?.emit('connection', ws, request)); + }); this._wsServer.on('connection', (ws, request) => { debugLogger.log('server', 'Connected client ws.extension=' + ws.extensions); const url = new URL('http://localhost' + (request.url || '')); @@ -171,8 +189,10 @@ export class PlaywrightServer { })); await waitForClose; debugLogger.log('server', 'closing http server'); - await new Promise(f => server.options.server!.close(f)); + if (this._server) + await new Promise(f => this._server!.close(f)); this._wsServer = undefined; + this._server = undefined; debugLogger.log('server', 'closed server'); debugLogger.log('server', 'closing browsers'); diff --git a/packages/playwright-core/src/utils/userAgent.ts b/packages/playwright-core/src/utils/userAgent.ts index f2c2cb0037..17b1bc51c8 100644 --- a/packages/playwright-core/src/utils/userAgent.ts +++ b/packages/playwright-core/src/utils/userAgent.ts @@ -17,6 +17,7 @@ import { execSync } from 'child_process'; import os from 'os'; import { getLinuxDistributionInfoSync } from '../utils/linuxUtils'; +import { wrapInASCIIBox } from './ascii'; let cachedUserAgent: string | undefined; @@ -76,8 +77,31 @@ export function getEmbedderName(): { embedderName: string, embedderVersion: stri } export function getPlaywrightVersion(majorMinorOnly = false): string { - const packageJson = require('./../../package.json'); - if (process.env.PW_VERSION_OVERRIDE) - return process.env.PW_VERSION_OVERRIDE; - return majorMinorOnly ? packageJson.version.split('.').slice(0, 2).join('.') : packageJson.version; + const version = process.env.PW_VERSION_OVERRIDE || require('./../../package.json').version; + return majorMinorOnly ? version.split('.').slice(0, 2).join('.') : version; +} + +export function userAgentVersionMatchesErrorMessage(userAgent: string) { + const match = userAgent.match(/^Playwright\/(\d+\.\d+\.\d+)/); + if (!match) { + // Cannot parse user agent - be lax. + return; + } + const received = match[1].split('.').slice(0, 2).join('.'); + const expected = getPlaywrightVersion(true); + if (received !== expected) { + return wrapInASCIIBox([ + `Playwright version mismatch:`, + ` - server version: v${expected}`, + ` - client version: v${received}`, + ``, + `If you are using VSCode extension, restart VSCode.`, + ``, + `If you are connecting to a remote service,`, + `keep your local Playwright version in sync`, + `with the remote service version.`, + ``, + `<3 Playwright Team` + ].join('\n'), 1); + } } diff --git a/tests/config/remoteServer.ts b/tests/config/remoteServer.ts index 2b81abba7c..34f6dcb4ca 100644 --- a/tests/config/remoteServer.ts +++ b/tests/config/remoteServer.ts @@ -27,7 +27,7 @@ export class RunServer implements PlaywrightServer { private _process: TestChildProcess; _wsEndpoint: string; - async start(childProcess: CommonFixtures['childProcess'], mode?: 'extension' | 'default') { + async start(childProcess: CommonFixtures['childProcess'], mode?: 'extension' | 'default', env?: NodeJS.ProcessEnv) { const command = ['node', path.join(__dirname, '..', '..', 'packages', 'playwright-core', 'lib', 'cli', 'cli.js'), 'run-server']; if (mode === 'extension') command.push('--mode=extension'); @@ -36,6 +36,7 @@ export class RunServer implements PlaywrightServer { env: { ...process.env, PWTEST_UNDER_TEST: '1', + ...env, }, }); diff --git a/tests/library/browsertype-connect.spec.ts b/tests/library/browsertype-connect.spec.ts index 89d58035de..b5d480c5d8 100644 --- a/tests/library/browsertype-connect.spec.ts +++ b/tests/library/browsertype-connect.spec.ts @@ -20,7 +20,7 @@ import os from 'os'; import type http from 'http'; import type net from 'net'; import * as path from 'path'; -import { getUserAgent } from '../../packages/playwright-core/lib/utils/userAgent'; +import { getUserAgent, getPlaywrightVersion } from '../../packages/playwright-core/lib/utils/userAgent'; import WebSocket from 'ws'; import { expect, playwrightTest } from '../config/browserTest'; import { parseTrace, suppressCertificateWarning } from '../config/utils'; @@ -28,6 +28,7 @@ import formidable from 'formidable'; import type { Browser, ConnectOptions } from 'playwright-core'; import { createHttpServer } from '../../packages/playwright-core/lib/utils/network'; import { kTargetClosedErrorMessage } from '../config/errors'; +import { RunServer } from '../config/remoteServer'; type ExtraFixtures = { connect: (wsEndpoint: string, options?: ConnectOptions, redirectPortForTest?: number) => Promise, @@ -914,3 +915,13 @@ test.describe('launchServer only', () => { } }); }); + +test('should refuse connecting when versions do not match', async ({ connect, childProcess }) => { + const server = new RunServer(); + await server.start(childProcess, 'default', { PW_VERSION_OVERRIDE: '1.2.3' }); + const error = await connect(server.wsEndpoint()).catch(e => e); + await server.close(); + expect(error.message).toContain('Playwright version mismatch'); + expect(error.message).toContain('server version: v1.2'); + expect(error.message).toContain('client version: v' + getPlaywrightVersion(true)); +});