mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-01 08:34:02 +03:00
chore: client certificates refactorings (#31822)
This commit is contained in:
parent
f23d02a211
commit
b9c4b6bff0
@ -35,7 +35,6 @@ class SocksProxyConnection {
|
|||||||
target!: net.Socket;
|
target!: net.Socket;
|
||||||
// In case of http, we just pipe data to the target socket and they are |undefined|.
|
// In case of http, we just pipe data to the target socket and they are |undefined|.
|
||||||
internal: stream.Duplex | undefined;
|
internal: stream.Duplex | undefined;
|
||||||
internalTLS: tls.TLSSocket | undefined;
|
|
||||||
|
|
||||||
constructor(socksProxy: ClientCertificatesProxy, uid: string, host: string, port: number) {
|
constructor(socksProxy: ClientCertificatesProxy, uid: string, host: string, port: number) {
|
||||||
this.socksProxy = socksProxy;
|
this.socksProxy = socksProxy;
|
||||||
@ -85,50 +84,51 @@ class SocksProxyConnection {
|
|||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const internalTLS = new tls.TLSSocket(this.internal, {
|
const dummyServer = tls.createServer({
|
||||||
isServer: true,
|
|
||||||
key: fs.readFileSync(path.join(__dirname, '../../bin/socks-certs/key.pem')),
|
key: fs.readFileSync(path.join(__dirname, '../../bin/socks-certs/key.pem')),
|
||||||
cert: fs.readFileSync(path.join(__dirname, '../../bin/socks-certs/cert.pem')),
|
cert: fs.readFileSync(path.join(__dirname, '../../bin/socks-certs/cert.pem')),
|
||||||
});
|
});
|
||||||
this.internalTLS = internalTLS;
|
dummyServer.emit('connection', this.internal);
|
||||||
internalTLS.on('close', () => this.socksProxy._socksProxy.sendSocketEnd({ uid: this.uid }));
|
dummyServer.on('secureConnection', internalTLS => {
|
||||||
|
internalTLS.on('close', () => this.socksProxy._socksProxy.sendSocketEnd({ uid: this.uid }));
|
||||||
|
|
||||||
const tlsOptions: tls.ConnectionOptions = {
|
const tlsOptions: tls.ConnectionOptions = {
|
||||||
socket: this.target,
|
socket: this.target,
|
||||||
host: this.host,
|
host: this.host,
|
||||||
port: this.port,
|
port: this.port,
|
||||||
rejectUnauthorized: !this.socksProxy.ignoreHTTPSErrors,
|
rejectUnauthorized: !this.socksProxy.ignoreHTTPSErrors,
|
||||||
...clientCertificatesToTLSOptions(this.socksProxy.clientCertificates, `https://${this.host}:${this.port}/`),
|
...clientCertificatesToTLSOptions(this.socksProxy.clientCertificates, `https://${this.host}:${this.port}/`),
|
||||||
};
|
};
|
||||||
if (!net.isIP(this.host))
|
if (!net.isIP(this.host))
|
||||||
tlsOptions.servername = this.host;
|
tlsOptions.servername = this.host;
|
||||||
if (process.env.PWTEST_UNSUPPORTED_CUSTOM_CA && isUnderTest())
|
if (process.env.PWTEST_UNSUPPORTED_CUSTOM_CA && isUnderTest())
|
||||||
tlsOptions.ca = [fs.readFileSync(process.env.PWTEST_UNSUPPORTED_CUSTOM_CA)];
|
tlsOptions.ca = [fs.readFileSync(process.env.PWTEST_UNSUPPORTED_CUSTOM_CA)];
|
||||||
const targetTLS = tls.connect(tlsOptions);
|
const targetTLS = tls.connect(tlsOptions);
|
||||||
|
|
||||||
internalTLS.pipe(targetTLS);
|
internalTLS.pipe(targetTLS);
|
||||||
targetTLS.pipe(internalTLS);
|
targetTLS.pipe(internalTLS);
|
||||||
|
|
||||||
// Handle close and errors
|
// Handle close and errors
|
||||||
const closeBothSockets = () => {
|
const closeBothSockets = () => {
|
||||||
internalTLS.end();
|
internalTLS.end();
|
||||||
targetTLS.end();
|
targetTLS.end();
|
||||||
};
|
};
|
||||||
|
|
||||||
internalTLS.on('end', () => closeBothSockets());
|
internalTLS.on('end', () => closeBothSockets());
|
||||||
targetTLS.on('end', () => closeBothSockets());
|
targetTLS.on('end', () => closeBothSockets());
|
||||||
|
|
||||||
internalTLS.on('error', () => closeBothSockets());
|
internalTLS.on('error', () => closeBothSockets());
|
||||||
targetTLS.on('error', error => {
|
targetTLS.on('error', error => {
|
||||||
const responseBody = 'Playwright client-certificate error: ' + error.message;
|
const responseBody = 'Playwright client-certificate error: ' + error.message;
|
||||||
internalTLS.end([
|
internalTLS.end([
|
||||||
'HTTP/1.1 503 Internal Server Error',
|
'HTTP/1.1 503 Internal Server Error',
|
||||||
'Content-Type: text/html; charset=utf-8',
|
'Content-Type: text/html; charset=utf-8',
|
||||||
'Content-Length: ' + Buffer.byteLength(responseBody),
|
'Content-Length: ' + Buffer.byteLength(responseBody),
|
||||||
'\r\n',
|
'\r\n',
|
||||||
responseBody,
|
responseBody,
|
||||||
].join('\r\n'));
|
].join('\r\n'));
|
||||||
closeBothSockets();
|
closeBothSockets();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,48 +15,55 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import http2 from 'http2';
|
||||||
|
import type http from 'http';
|
||||||
import { expect, playwrightTest as base } from '../config/browserTest';
|
import { expect, playwrightTest as base } from '../config/browserTest';
|
||||||
import type net from 'net';
|
import type net from 'net';
|
||||||
import type { BrowserContextOptions } from 'packages/playwright-test';
|
import type { BrowserContextOptions } from 'packages/playwright-test';
|
||||||
const { createHttpsServer } = require('../../packages/playwright-core/lib/utils');
|
const { createHttpsServer } = require('../../packages/playwright-core/lib/utils');
|
||||||
|
|
||||||
const test = base.extend<{ serverURL: string, serverURLRewrittenToLocalhost: string }>({
|
type TestOptions = {
|
||||||
serverURL: async ({ asset }, use) => {
|
startCCServer(options?: {
|
||||||
const server = createHttpsServer({
|
http2?: boolean;
|
||||||
key: fs.readFileSync(asset('client-certificates/server/server_key.pem')),
|
useFakeLocalhost?: boolean;
|
||||||
cert: fs.readFileSync(asset('client-certificates/server/server_cert.pem')),
|
}): Promise<string>,
|
||||||
ca: [
|
};
|
||||||
fs.readFileSync(asset('client-certificates/server/server_cert.pem')),
|
|
||||||
],
|
const test = base.extend<TestOptions>({
|
||||||
requestCert: true,
|
startCCServer: async ({ asset, browserName }, use) => {
|
||||||
rejectUnauthorized: false,
|
|
||||||
}, (req, res) => {
|
|
||||||
const tlsSocket = req.socket as import('tls').TLSSocket;
|
|
||||||
// @ts-expect-error
|
|
||||||
expect(['localhost', 'local.playwright'].includes((tlsSocket).servername)).toBe(true);
|
|
||||||
const cert = tlsSocket.getPeerCertificate();
|
|
||||||
if ((req as any).client.authorized) {
|
|
||||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
||||||
res.end(`Hello ${cert.subject.CN}, your certificate was issued by ${cert.issuer.CN}!`);
|
|
||||||
} else if (cert.subject) {
|
|
||||||
res.writeHead(403, { 'Content-Type': 'text/html' });
|
|
||||||
res.end(`Sorry ${cert.subject.CN}, certificates from ${cert.issuer.CN} are not welcome here.`);
|
|
||||||
} else {
|
|
||||||
res.writeHead(401, { 'Content-Type': 'text/html' });
|
|
||||||
res.end(`Sorry, but you need to provide a client certificate to continue.`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
process.env.PWTEST_UNSUPPORTED_CUSTOM_CA = asset('client-certificates/server/server_cert.pem');
|
process.env.PWTEST_UNSUPPORTED_CUSTOM_CA = asset('client-certificates/server/server_cert.pem');
|
||||||
await new Promise<void>(f => server.listen(0, 'localhost', () => f()));
|
let server: http.Server | http2.Http2Server | undefined;
|
||||||
await use(`https://localhost:${(server.address() as net.AddressInfo).port}/`);
|
await use(async options => {
|
||||||
|
server = (options?.http2 ? http2.createSecureServer : createHttpsServer)({
|
||||||
|
key: fs.readFileSync(asset('client-certificates/server/server_key.pem')),
|
||||||
|
cert: fs.readFileSync(asset('client-certificates/server/server_cert.pem')),
|
||||||
|
ca: [
|
||||||
|
fs.readFileSync(asset('client-certificates/server/server_cert.pem')),
|
||||||
|
],
|
||||||
|
requestCert: true,
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
}, (req: (http2.Http2ServerRequest | http.IncomingMessage), res: http2.Http2ServerResponse | http.ServerResponse) => {
|
||||||
|
const tlsSocket = req.socket as import('tls').TLSSocket;
|
||||||
|
// @ts-expect-error https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/62336
|
||||||
|
expect(['localhost', 'local.playwright'].includes((tlsSocket).servername)).toBe(true);
|
||||||
|
const cert = tlsSocket.getPeerCertificate();
|
||||||
|
if (tlsSocket.authorized) {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||||
|
res.end(`Hello ${cert.subject.CN}, your certificate was issued by ${cert.issuer.CN}!`);
|
||||||
|
} else if (cert.subject) {
|
||||||
|
res.writeHead(403, { 'Content-Type': 'text/html' });
|
||||||
|
res.end(`Sorry ${cert.subject.CN}, certificates from ${cert.issuer.CN} are not welcome here.`);
|
||||||
|
} else {
|
||||||
|
res.writeHead(401, { 'Content-Type': 'text/html' });
|
||||||
|
res.end(`Sorry, but you need to provide a client certificate to continue.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await new Promise<void>(f => server.listen(0, 'localhost', () => f()));
|
||||||
|
const host = options?.useFakeLocalhost ? 'local.playwright' : 'localhost';
|
||||||
|
return `https://${host}:${(server.address() as net.AddressInfo).port}/`;
|
||||||
|
});
|
||||||
await new Promise<void>(resolve => server.close(() => resolve()));
|
await new Promise<void>(resolve => server.close(() => resolve()));
|
||||||
},
|
},
|
||||||
serverURLRewrittenToLocalhost: async ({ serverURL, browserName }, use) => {
|
|
||||||
const parsed = new URL(serverURL);
|
|
||||||
parsed.hostname = 'local.playwright';
|
|
||||||
const shouldRewriteToLocalhost = browserName === 'webkit' && process.platform === 'darwin';
|
|
||||||
await use(shouldRewriteToLocalhost ? parsed.toString() : serverURL);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test.use({
|
test.use({
|
||||||
@ -103,7 +110,8 @@ test.describe('fetch', () => {
|
|||||||
await expect(playwright.request.newContext(contextOptions)).rejects.toThrow(expected);
|
await expect(playwright.request.newContext(contextOptions)).rejects.toThrow(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should fail with no client certificates provided', async ({ playwright, serverURL }) => {
|
test('should fail with no client certificates provided', async ({ playwright, startCCServer }) => {
|
||||||
|
const serverURL = await startCCServer();
|
||||||
const request = await playwright.request.newContext();
|
const request = await playwright.request.newContext();
|
||||||
const response = await request.get(serverURL);
|
const response = await request.get(serverURL);
|
||||||
expect(response.status()).toBe(401);
|
expect(response.status()).toBe(401);
|
||||||
@ -128,7 +136,8 @@ test.describe('fetch', () => {
|
|||||||
await request.dispose();
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should throw with untrusted client certs', async ({ playwright, serverURL, asset }) => {
|
test('should throw with untrusted client certs', async ({ playwright, startCCServer, asset }) => {
|
||||||
|
const serverURL = await startCCServer();
|
||||||
const request = await playwright.request.newContext({
|
const request = await playwright.request.newContext({
|
||||||
clientCertificates: [{
|
clientCertificates: [{
|
||||||
url: serverURL,
|
url: serverURL,
|
||||||
@ -145,7 +154,8 @@ test.describe('fetch', () => {
|
|||||||
await request.dispose();
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('pass with trusted client certificates', async ({ playwright, serverURL, asset }) => {
|
test('pass with trusted client certificates', async ({ playwright, startCCServer, asset }) => {
|
||||||
|
const serverURL = await startCCServer();
|
||||||
const request = await playwright.request.newContext({
|
const request = await playwright.request.newContext({
|
||||||
clientCertificates: [{
|
clientCertificates: [{
|
||||||
url: serverURL,
|
url: serverURL,
|
||||||
@ -162,7 +172,8 @@ test.describe('fetch', () => {
|
|||||||
await request.dispose();
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should work in the browser with request interception', async ({ browser, playwright, serverURL, asset }) => {
|
test('should work in the browser with request interception', async ({ browser, playwright, startCCServer, asset }) => {
|
||||||
|
const serverURL = await startCCServer();
|
||||||
const request = await playwright.request.newContext({
|
const request = await playwright.request.newContext({
|
||||||
clientCertificates: [{
|
clientCertificates: [{
|
||||||
url: serverURL,
|
url: serverURL,
|
||||||
@ -207,7 +218,8 @@ test.describe('browser', () => {
|
|||||||
await page.close();
|
await page.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should fail with no client certificates', async ({ browser, serverURLRewrittenToLocalhost, asset }) => {
|
test('should fail with no client certificates', async ({ browser, startCCServer, asset, browserName }) => {
|
||||||
|
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
||||||
const page = await browser.newPage({
|
const page = await browser.newPage({
|
||||||
clientCertificates: [{
|
clientCertificates: [{
|
||||||
url: 'https://not-matching.com',
|
url: 'https://not-matching.com',
|
||||||
@ -217,37 +229,39 @@ test.describe('browser', () => {
|
|||||||
}],
|
}],
|
||||||
}],
|
}],
|
||||||
});
|
});
|
||||||
await page.goto(serverURLRewrittenToLocalhost);
|
await page.goto(serverURL);
|
||||||
await expect(page.getByText('Sorry, but you need to provide a client certificate to continue.')).toBeVisible();
|
await expect(page.getByText('Sorry, but you need to provide a client certificate to continue.')).toBeVisible();
|
||||||
await page.close();
|
await page.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should fail with self-signed client certificates', async ({ browser, serverURLRewrittenToLocalhost, asset }) => {
|
test('should fail with self-signed client certificates', async ({ browser, startCCServer, asset, browserName }) => {
|
||||||
|
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
||||||
const page = await browser.newPage({
|
const page = await browser.newPage({
|
||||||
clientCertificates: [{
|
clientCertificates: [{
|
||||||
url: serverURLRewrittenToLocalhost,
|
url: serverURL,
|
||||||
certs: [{
|
certs: [{
|
||||||
certPath: asset('client-certificates/client/self-signed/cert.pem'),
|
certPath: asset('client-certificates/client/self-signed/cert.pem'),
|
||||||
keyPath: asset('client-certificates/client/self-signed/key.pem'),
|
keyPath: asset('client-certificates/client/self-signed/key.pem'),
|
||||||
}],
|
}],
|
||||||
}],
|
}],
|
||||||
});
|
});
|
||||||
await page.goto(serverURLRewrittenToLocalhost);
|
await page.goto(serverURL);
|
||||||
await expect(page.getByText('Sorry Bob, certificates from Bob are not welcome here')).toBeVisible();
|
await expect(page.getByText('Sorry Bob, certificates from Bob are not welcome here')).toBeVisible();
|
||||||
await page.close();
|
await page.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should pass with matching certificates', async ({ browser, serverURLRewrittenToLocalhost, asset }) => {
|
test('should pass with matching certificates', async ({ browser, startCCServer, asset, browserName }) => {
|
||||||
|
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
||||||
const page = await browser.newPage({
|
const page = await browser.newPage({
|
||||||
clientCertificates: [{
|
clientCertificates: [{
|
||||||
url: serverURLRewrittenToLocalhost,
|
url: serverURL,
|
||||||
certs: [{
|
certs: [{
|
||||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||||
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
||||||
}],
|
}],
|
||||||
}],
|
}],
|
||||||
});
|
});
|
||||||
await page.goto(serverURLRewrittenToLocalhost);
|
await page.goto(serverURL);
|
||||||
await expect(page.getByText('Hello Alice, your certificate was issued by localhost!')).toBeVisible();
|
await expect(page.getByText('Hello Alice, your certificate was issued by localhost!')).toBeVisible();
|
||||||
await page.close();
|
await page.close();
|
||||||
});
|
});
|
||||||
@ -274,17 +288,18 @@ test.describe('browser', () => {
|
|||||||
await expect(launchPersistent(contextOptions)).rejects.toThrow(expected);
|
await expect(launchPersistent(contextOptions)).rejects.toThrow(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should pass with matching certificates', async ({ launchPersistent, serverURLRewrittenToLocalhost, asset }) => {
|
test('should pass with matching certificates', async ({ launchPersistent, startCCServer, asset, browserName }) => {
|
||||||
|
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
||||||
const { page } = await launchPersistent({
|
const { page } = await launchPersistent({
|
||||||
clientCertificates: [{
|
clientCertificates: [{
|
||||||
url: serverURLRewrittenToLocalhost,
|
url: serverURL,
|
||||||
certs: [{
|
certs: [{
|
||||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||||
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
||||||
}],
|
}],
|
||||||
}],
|
}],
|
||||||
});
|
});
|
||||||
await page.goto(serverURLRewrittenToLocalhost);
|
await page.goto(serverURL);
|
||||||
await expect(page.getByText('Hello Alice, your certificate was issued by localhost!')).toBeVisible();
|
await expect(page.getByText('Hello Alice, your certificate was issued by localhost!')).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user