feat: check client version on the server (#27585)

This commit is contained in:
Dmitry Gozman 2023-10-13 21:02:30 -07:00 committed by GitHub
parent f0167091e6
commit fc32ca676b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 66 additions and 10 deletions

View File

@ -27,7 +27,7 @@ import { ManualPromise } from '../utils/manualPromise';
import type { AndroidDevice } from '../server/android/android'; import type { AndroidDevice } from '../server/android/android';
import type { SocksProxy } from '../common/socksProxy'; import type { SocksProxy } from '../common/socksProxy';
import { debugLogger } from '../common/debugLogger'; import { debugLogger } from '../common/debugLogger';
import { createHttpServer } from '../utils'; import { createHttpServer, userAgentVersionMatchesErrorMessage } from '../utils';
import { perMessageDeflate } from '../server/transport'; import { perMessageDeflate } from '../server/transport';
let lastConnectionId = 0; let lastConnectionId = 0;
@ -45,6 +45,7 @@ type ServerOptions = {
export class PlaywrightServer { export class PlaywrightServer {
private _preLaunchedPlaywright: Playwright | undefined; private _preLaunchedPlaywright: Playwright | undefined;
private _wsServer: WebSocketServer | undefined; private _wsServer: WebSocketServer | undefined;
private _server: http.Server | undefined;
private _options: ServerOptions; private _options: ServerOptions;
constructor(options: ServerOptions) { constructor(options: ServerOptions) {
@ -69,6 +70,7 @@ export class PlaywrightServer {
response.end('Running'); response.end('Running');
}); });
server.on('error', error => debugLogger.log('server', String(error))); server.on('error', error => debugLogger.log('server', String(error)));
this._server = server;
const wsEndpoint = await new Promise<string>((resolve, reject) => { const wsEndpoint = await new Promise<string>((resolve, reject) => {
server.listen(port, () => { server.listen(port, () => {
@ -84,8 +86,7 @@ export class PlaywrightServer {
debugLogger.log('server', 'Listening at ' + wsEndpoint); debugLogger.log('server', 'Listening at ' + wsEndpoint);
this._wsServer = new wsServer({ this._wsServer = new wsServer({
server, noServer: true,
path: this._options.path,
perMessageDeflate, perMessageDeflate,
}); });
const browserSemaphore = new Semaphore(this._options.maxConnections); const browserSemaphore = new Semaphore(this._options.maxConnections);
@ -96,6 +97,23 @@ export class PlaywrightServer {
headers.push(process.env.PWTEST_SERVER_WS_HEADERS!); 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) => { this._wsServer.on('connection', (ws, request) => {
debugLogger.log('server', 'Connected client ws.extension=' + ws.extensions); debugLogger.log('server', 'Connected client ws.extension=' + ws.extensions);
const url = new URL('http://localhost' + (request.url || '')); const url = new URL('http://localhost' + (request.url || ''));
@ -171,8 +189,10 @@ export class PlaywrightServer {
})); }));
await waitForClose; await waitForClose;
debugLogger.log('server', 'closing http server'); 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._wsServer = undefined;
this._server = undefined;
debugLogger.log('server', 'closed server'); debugLogger.log('server', 'closed server');
debugLogger.log('server', 'closing browsers'); debugLogger.log('server', 'closing browsers');

View File

@ -17,6 +17,7 @@
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import os from 'os'; import os from 'os';
import { getLinuxDistributionInfoSync } from '../utils/linuxUtils'; import { getLinuxDistributionInfoSync } from '../utils/linuxUtils';
import { wrapInASCIIBox } from './ascii';
let cachedUserAgent: string | undefined; let cachedUserAgent: string | undefined;
@ -76,8 +77,31 @@ export function getEmbedderName(): { embedderName: string, embedderVersion: stri
} }
export function getPlaywrightVersion(majorMinorOnly = false): string { export function getPlaywrightVersion(majorMinorOnly = false): string {
const packageJson = require('./../../package.json'); const version = process.env.PW_VERSION_OVERRIDE || require('./../../package.json').version;
if (process.env.PW_VERSION_OVERRIDE) return majorMinorOnly ? version.split('.').slice(0, 2).join('.') : version;
return process.env.PW_VERSION_OVERRIDE; }
return majorMinorOnly ? packageJson.version.split('.').slice(0, 2).join('.') : packageJson.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);
}
} }

View File

@ -27,7 +27,7 @@ export class RunServer implements PlaywrightServer {
private _process: TestChildProcess; private _process: TestChildProcess;
_wsEndpoint: string; _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']; const command = ['node', path.join(__dirname, '..', '..', 'packages', 'playwright-core', 'lib', 'cli', 'cli.js'), 'run-server'];
if (mode === 'extension') if (mode === 'extension')
command.push('--mode=extension'); command.push('--mode=extension');
@ -36,6 +36,7 @@ export class RunServer implements PlaywrightServer {
env: { env: {
...process.env, ...process.env,
PWTEST_UNDER_TEST: '1', PWTEST_UNDER_TEST: '1',
...env,
}, },
}); });

View File

@ -20,7 +20,7 @@ import os from 'os';
import type http from 'http'; import type http from 'http';
import type net from 'net'; import type net from 'net';
import * as path from 'path'; 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 WebSocket from 'ws';
import { expect, playwrightTest } from '../config/browserTest'; import { expect, playwrightTest } from '../config/browserTest';
import { parseTrace, suppressCertificateWarning } from '../config/utils'; import { parseTrace, suppressCertificateWarning } from '../config/utils';
@ -28,6 +28,7 @@ import formidable from 'formidable';
import type { Browser, ConnectOptions } from 'playwright-core'; import type { Browser, ConnectOptions } from 'playwright-core';
import { createHttpServer } from '../../packages/playwright-core/lib/utils/network'; import { createHttpServer } from '../../packages/playwright-core/lib/utils/network';
import { kTargetClosedErrorMessage } from '../config/errors'; import { kTargetClosedErrorMessage } from '../config/errors';
import { RunServer } from '../config/remoteServer';
type ExtraFixtures = { type ExtraFixtures = {
connect: (wsEndpoint: string, options?: ConnectOptions, redirectPortForTest?: number) => Promise<Browser>, connect: (wsEndpoint: string, options?: ConnectOptions, redirectPortForTest?: number) => Promise<Browser>,
@ -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));
});