2024-07-12 11:42:24 +02:00
/ * *
* Copyright ( c ) Microsoft Corporation . All rights reserved .
*
* 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 .
* /
import fs from 'fs' ;
2024-08-15 08:51:40 +02:00
import tls from 'tls' ;
2024-08-22 08:42:09 +02:00
import type https from 'https' ;
import zlib from 'zlib' ;
2024-07-25 18:53:38 +02:00
import type http2 from 'http2' ;
2024-07-23 19:18:31 +02:00
import type http from 'http' ;
2024-07-12 11:42:24 +02:00
import { expect , playwrightTest as base } from '../config/browserTest' ;
import type net from 'net' ;
import type { BrowserContextOptions } from 'packages/playwright-test' ;
2024-07-25 18:53:38 +02:00
const { createHttpsServer , createHttp2Server } = require ( '../../packages/playwright-core/lib/utils' ) ;
2024-07-12 11:42:24 +02:00
2024-07-23 19:18:31 +02:00
type TestOptions = {
startCCServer ( options ? : {
2024-07-29 16:44:53 +02:00
host? : string ;
2024-07-23 19:18:31 +02:00
http2? : boolean ;
2024-07-26 11:28:45 +02:00
enableHTTP1FallbackWhenUsingHttp2? : boolean ;
2024-07-23 19:18:31 +02:00
useFakeLocalhost? : boolean ;
} ) : Promise < string > ,
} ;
const test = base . extend < TestOptions > ( {
2024-07-25 18:53:38 +02:00
startCCServer : async ( { asset } , use ) = > {
2024-07-12 11:42:24 +02:00
process . env . PWTEST_UNSUPPORTED_CUSTOM_CA = asset ( 'client-certificates/server/server_cert.pem' ) ;
2024-07-25 18:53:38 +02:00
let server : http.Server | http2 . Http2SecureServer | undefined ;
2024-07-23 19:18:31 +02:00
await use ( async options = > {
2024-07-25 18:53:38 +02:00
server = ( options ? . http2 ? createHttp2Server : createHttpsServer ) ( {
2024-07-23 19:18:31 +02:00
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 ,
2024-07-26 11:28:45 +02:00
allowHTTP1 : options?.enableHTTP1FallbackWhenUsingHttp2 ,
2024-07-23 19:18:31 +02:00
} , ( req : ( http2 . Http2ServerRequest | http . IncomingMessage ) , res : http2.Http2ServerResponse | http . ServerResponse ) = > {
const tlsSocket = req . socket as import ( 'tls' ) . TLSSocket ;
2024-07-25 18:53:38 +02:00
const parts : { key : string , value : any } [ ] = [ ] ;
parts . push ( { key : 'alpn-protocol' , value : tlsSocket.alpnProtocol } ) ;
2024-07-23 19:18:31 +02:00
// @ts-expect-error https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/62336
2024-07-25 18:53:38 +02:00
parts . push ( { key : 'servername' , value : tlsSocket.servername } ) ;
2024-07-23 19:18:31 +02:00
const cert = tlsSocket . getPeerCertificate ( ) ;
if ( tlsSocket . authorized ) {
res . writeHead ( 200 , { 'Content-Type' : 'text/html' } ) ;
2024-07-25 18:53:38 +02:00
parts . push ( { key : 'message' , value : ` Hello ${ cert . subject . CN } , your certificate was issued by ${ cert . issuer . CN } ! ` } ) ;
2024-07-23 19:18:31 +02:00
} else if ( cert . subject ) {
res . writeHead ( 403 , { 'Content-Type' : 'text/html' } ) ;
2024-07-25 18:53:38 +02:00
parts . push ( { key : 'message' , value : ` Sorry ${ cert . subject . CN } , certificates from ${ cert . issuer . CN } are not welcome here. ` } ) ;
2024-07-23 19:18:31 +02:00
} else {
res . writeHead ( 401 , { 'Content-Type' : 'text/html' } ) ;
2024-07-25 18:53:38 +02:00
parts . push ( { key : 'message' , value : ` Sorry, but you need to provide a client certificate to continue. ` } ) ;
2024-07-23 19:18:31 +02:00
}
2024-07-25 18:53:38 +02:00
res . end ( parts . map ( ( { key , value } ) = > ` <div data-testid=" ${ key } "> ${ value } </div> ` ) . join ( '' ) ) ;
2024-07-23 19:18:31 +02:00
} ) ;
2024-07-29 16:44:53 +02:00
await new Promise < void > ( f = > server . listen ( 0 , options ? . host ? ? 'localhost' , ( ) = > f ( ) ) ) ;
2024-07-23 19:18:31 +02:00
const host = options ? . useFakeLocalhost ? 'local.playwright' : 'localhost' ;
return ` https:// ${ host } : ${ ( server . address ( ) as net . AddressInfo ) . port } / ` ;
} ) ;
2024-07-24 11:39:39 +02:00
if ( server )
await new Promise < void > ( resolve = > server . close ( ( ) = > resolve ( ) ) ) ;
2024-07-12 11:42:24 +02:00
} ,
} ) ;
2024-07-18 13:43:56 +02:00
test . use ( {
launchOptions : async ( { launchOptions } , use ) = > {
await use ( {
. . . launchOptions ,
proxy : { server : 'per-context' }
} ) ;
}
} ) ;
2024-07-12 11:42:24 +02:00
const kDummyFileName = __filename ;
const kValidationSubTests : [ BrowserContextOptions , string ] [ ] = [
2024-07-23 22:56:36 +02:00
[ { clientCertificates : [ { origin : 'test' } ] } , 'None of cert, key, passphrase or pfx is specified' ] ,
2024-07-12 11:42:24 +02:00
[ {
clientCertificates : [ {
2024-07-23 22:56:36 +02:00
origin : 'test' ,
certPath : kDummyFileName ,
keyPath : kDummyFileName ,
pfxPath : kDummyFileName ,
passphrase : kDummyFileName ,
2024-07-12 11:42:24 +02:00
} ]
} , 'pfx is specified together with cert, key or passphrase' ] ,
[ {
proxy : { server : 'http://localhost:8080' } ,
clientCertificates : [ {
2024-07-23 22:56:36 +02:00
origin : 'test' ,
certPath : kDummyFileName ,
keyPath : kDummyFileName ,
2024-07-12 11:42:24 +02:00
} ]
} , 'Cannot specify both proxy and clientCertificates' ] ,
] ;
test . describe ( 'fetch' , ( ) = > {
test ( 'validate input' , async ( { playwright } ) = > {
for ( const [ contextOptions , expected ] of kValidationSubTests )
await expect ( playwright . request . newContext ( contextOptions ) ) . rejects . toThrow ( expected ) ;
} ) ;
2024-07-23 19:18:31 +02:00
test ( 'should fail with no client certificates provided' , async ( { playwright , startCCServer } ) = > {
const serverURL = await startCCServer ( ) ;
2024-08-02 08:34:28 +02:00
const request = await playwright . request . newContext ( { ignoreHTTPSErrors : true } ) ;
2024-07-12 11:42:24 +02:00
const response = await request . get ( serverURL ) ;
expect ( response . status ( ) ) . toBe ( 401 ) ;
2024-07-24 11:39:39 +02:00
expect ( await response . text ( ) ) . toContain ( 'Sorry, but you need to provide a client certificate to continue.' ) ;
2024-07-12 11:42:24 +02:00
await request . dispose ( ) ;
} ) ;
test ( 'should keep supporting http' , async ( { playwright , server , asset } ) = > {
const request = await playwright . request . newContext ( {
2024-08-02 08:34:28 +02:00
ignoreHTTPSErrors : true ,
2024-07-12 11:42:24 +02:00
clientCertificates : [ {
2024-07-23 22:56:36 +02:00
origin : new URL ( server . PREFIX ) . origin ,
certPath : asset ( 'client-certificates/client/trusted/cert.pem' ) ,
keyPath : asset ( 'client-certificates/client/trusted/key.pem' ) ,
2024-07-12 11:42:24 +02:00
} ] ,
} ) ;
const response = await request . get ( server . PREFIX + '/one-style.html' ) ;
expect ( response . url ( ) ) . toBe ( server . PREFIX + '/one-style.html' ) ;
expect ( response . status ( ) ) . toBe ( 200 ) ;
expect ( await response . text ( ) ) . toContain ( '<div>hello, world!</div>' ) ;
await request . dispose ( ) ;
} ) ;
2024-07-23 19:18:31 +02:00
test ( 'should throw with untrusted client certs' , async ( { playwright , startCCServer , asset } ) = > {
const serverURL = await startCCServer ( ) ;
2024-07-12 11:42:24 +02:00
const request = await playwright . request . newContext ( {
2024-08-02 08:34:28 +02:00
ignoreHTTPSErrors : true ,
2024-07-12 11:42:24 +02:00
clientCertificates : [ {
2024-07-23 22:56:36 +02:00
origin : new URL ( serverURL ) . origin ,
certPath : asset ( 'client-certificates/client/self-signed/cert.pem' ) ,
keyPath : asset ( 'client-certificates/client/self-signed/key.pem' ) ,
2024-07-12 11:42:24 +02:00
} ] ,
} ) ;
const response = await request . get ( serverURL ) ;
expect ( response . url ( ) ) . toBe ( serverURL ) ;
expect ( response . status ( ) ) . toBe ( 403 ) ;
2024-07-24 11:39:39 +02:00
expect ( await response . text ( ) ) . toContain ( 'Sorry Bob, certificates from Bob are not welcome here.' ) ;
2024-07-12 11:42:24 +02:00
await request . dispose ( ) ;
} ) ;
2024-07-23 19:18:31 +02:00
test ( 'pass with trusted client certificates' , async ( { playwright , startCCServer , asset } ) = > {
const serverURL = await startCCServer ( ) ;
2024-07-12 11:42:24 +02:00
const request = await playwright . request . newContext ( {
2024-08-02 08:34:28 +02:00
ignoreHTTPSErrors : true ,
2024-07-12 11:42:24 +02:00
clientCertificates : [ {
2024-07-23 22:56:36 +02:00
origin : new URL ( serverURL ) . origin ,
certPath : asset ( 'client-certificates/client/trusted/cert.pem' ) ,
keyPath : asset ( 'client-certificates/client/trusted/key.pem' ) ,
2024-07-12 11:42:24 +02:00
} ] ,
} ) ;
const response = await request . get ( serverURL ) ;
expect ( response . url ( ) ) . toBe ( serverURL ) ;
expect ( response . status ( ) ) . toBe ( 200 ) ;
2024-07-24 11:39:39 +02:00
expect ( await response . text ( ) ) . toContain ( 'Hello Alice, your certificate was issued by localhost!' ) ;
2024-07-12 11:42:24 +02:00
await request . dispose ( ) ;
} ) ;
2024-08-05 14:42:29 +02:00
test ( 'pass with trusted client certificates in pfx format' , async ( { playwright , startCCServer , asset } ) = > {
const serverURL = await startCCServer ( ) ;
const request = await playwright . request . newContext ( {
ignoreHTTPSErrors : true ,
clientCertificates : [ {
origin : new URL ( serverURL ) . origin ,
pfxPath : asset ( 'client-certificates/client/trusted/cert.pfx' ) ,
passphrase : 'secure'
} ] ,
} ) ;
const response = await request . get ( serverURL ) ;
expect ( response . url ( ) ) . toBe ( serverURL ) ;
expect ( response . status ( ) ) . toBe ( 200 ) ;
expect ( await response . text ( ) ) . toContain ( 'Hello Alice, your certificate was issued by localhost!' ) ;
await request . dispose ( ) ;
} ) ;
test ( 'should throw a http error if the pfx passphrase is incorect' , async ( { playwright , startCCServer , asset , browserName } ) = > {
2024-08-06 08:46:35 +02:00
const serverURL = await startCCServer ( ) ;
2024-08-05 14:42:29 +02:00
const request = await playwright . request . newContext ( {
ignoreHTTPSErrors : true ,
clientCertificates : [ {
origin : new URL ( serverURL ) . origin ,
pfxPath : asset ( 'client-certificates/client/trusted/cert.pfx' ) ,
passphrase : 'this-password-is-incorrect'
} ] ,
} ) ;
await expect ( request . get ( serverURL ) ) . rejects . toThrow ( 'mac verify failure' ) ;
await request . dispose ( ) ;
} ) ;
test ( 'should fail with matching certificates in legacy pfx format' , async ( { playwright , startCCServer , asset , browserName } ) = > {
2024-08-06 08:46:35 +02:00
const serverURL = await startCCServer ( ) ;
2024-08-05 14:42:29 +02:00
const request = await playwright . request . newContext ( {
ignoreHTTPSErrors : true ,
clientCertificates : [ {
origin : new URL ( serverURL ) . origin ,
pfxPath : asset ( 'client-certificates/client/trusted/cert-legacy.pfx' ) ,
passphrase : 'secure'
} ] ,
} ) ;
await expect ( request . get ( serverURL ) ) . rejects . toThrow ( 'Unsupported TLS certificate' ) ;
await request . dispose ( ) ;
} ) ;
2024-07-23 19:18:31 +02:00
test ( 'should work in the browser with request interception' , async ( { browser , playwright , startCCServer , asset } ) = > {
const serverURL = await startCCServer ( ) ;
2024-07-12 11:42:24 +02:00
const request = await playwright . request . newContext ( {
2024-08-02 08:34:28 +02:00
ignoreHTTPSErrors : true ,
2024-07-12 11:42:24 +02:00
clientCertificates : [ {
2024-07-23 22:56:36 +02:00
origin : new URL ( serverURL ) . origin ,
certPath : asset ( 'client-certificates/client/trusted/cert.pem' ) ,
keyPath : asset ( 'client-certificates/client/trusted/key.pem' ) ,
2024-07-12 11:42:24 +02:00
} ] ,
} ) ;
const page = await browser . newPage ( { ignoreHTTPSErrors : true } ) ;
await page . route ( '**/*' , async route = > {
const response = await request . fetch ( route . request ( ) ) ;
await route . fulfill ( { response } ) ;
} ) ;
await page . goto ( serverURL ) ;
2024-07-25 18:53:38 +02:00
await expect ( page . getByTestId ( 'message' ) ) . toHaveText ( 'Hello Alice, your certificate was issued by localhost!' ) ;
2024-07-12 11:42:24 +02:00
await page . close ( ) ;
await request . dispose ( ) ;
} ) ;
} ) ;
test . describe ( 'browser' , ( ) = > {
test ( 'validate input' , async ( { browser } ) = > {
for ( const [ contextOptions , expected ] of kValidationSubTests )
await expect ( browser . newContext ( contextOptions ) ) . rejects . toThrow ( expected ) ;
} ) ;
test ( 'should keep supporting http' , async ( { browser , server , asset } ) = > {
const page = await browser . newPage ( {
clientCertificates : [ {
2024-07-23 22:56:36 +02:00
origin : new URL ( server . PREFIX ) . origin ,
certPath : asset ( 'client-certificates/client/trusted/cert.pem' ) ,
keyPath : asset ( 'client-certificates/client/trusted/key.pem' ) ,
2024-07-12 11:42:24 +02:00
} ] ,
} ) ;
await page . goto ( server . PREFIX + '/one-style.html' ) ;
await expect ( page . getByText ( 'hello, world!' ) ) . toBeVisible ( ) ;
await expect ( page . locator ( 'body' ) ) . toHaveCSS ( 'background-color' , 'rgb(255, 192, 203)' ) ;
await page . close ( ) ;
} ) ;
2024-07-23 19:18:31 +02:00
test ( 'should fail with no client certificates' , async ( { browser , startCCServer , asset , browserName } ) = > {
const serverURL = await startCCServer ( { useFakeLocalhost : browserName === 'webkit' && process . platform === 'darwin' } ) ;
2024-07-12 11:42:24 +02:00
const page = await browser . newPage ( {
2024-08-02 08:34:28 +02:00
ignoreHTTPSErrors : true ,
2024-07-12 11:42:24 +02:00
clientCertificates : [ {
2024-07-23 22:56:36 +02:00
origin : 'https://not-matching.com' ,
certPath : asset ( 'client-certificates/client/trusted/cert.pem' ) ,
keyPath : asset ( 'client-certificates/client/trusted/key.pem' ) ,
2024-07-12 11:42:24 +02:00
} ] ,
} ) ;
2024-07-23 19:18:31 +02:00
await page . goto ( serverURL ) ;
2024-07-25 18:53:38 +02:00
await expect ( page . getByTestId ( 'message' ) ) . toHaveText ( 'Sorry, but you need to provide a client certificate to continue.' ) ;
2024-07-12 11:42:24 +02:00
await page . close ( ) ;
} ) ;
2024-07-23 19:18:31 +02:00
test ( 'should fail with self-signed client certificates' , async ( { browser , startCCServer , asset , browserName } ) = > {
const serverURL = await startCCServer ( { useFakeLocalhost : browserName === 'webkit' && process . platform === 'darwin' } ) ;
2024-07-12 11:42:24 +02:00
const page = await browser . newPage ( {
2024-08-02 08:34:28 +02:00
ignoreHTTPSErrors : true ,
2024-07-12 11:42:24 +02:00
clientCertificates : [ {
2024-07-23 22:56:36 +02:00
origin : new URL ( serverURL ) . origin ,
certPath : asset ( 'client-certificates/client/self-signed/cert.pem' ) ,
keyPath : asset ( 'client-certificates/client/self-signed/key.pem' ) ,
2024-07-12 11:42:24 +02:00
} ] ,
} ) ;
2024-07-23 19:18:31 +02:00
await page . goto ( serverURL ) ;
2024-07-25 18:53:38 +02:00
await expect ( page . getByTestId ( 'message' ) ) . toHaveText ( 'Sorry Bob, certificates from Bob are not welcome here.' ) ;
2024-07-12 11:42:24 +02:00
await page . close ( ) ;
} ) ;
2024-07-23 19:18:31 +02:00
test ( 'should pass with matching certificates' , async ( { browser , startCCServer , asset , browserName } ) = > {
const serverURL = await startCCServer ( { useFakeLocalhost : browserName === 'webkit' && process . platform === 'darwin' } ) ;
2024-07-12 11:42:24 +02:00
const page = await browser . newPage ( {
2024-08-02 08:34:28 +02:00
ignoreHTTPSErrors : true ,
2024-07-12 11:42:24 +02:00
clientCertificates : [ {
2024-07-23 22:56:36 +02:00
origin : new URL ( serverURL ) . origin ,
certPath : asset ( 'client-certificates/client/trusted/cert.pem' ) ,
keyPath : asset ( 'client-certificates/client/trusted/key.pem' ) ,
2024-07-12 11:42:24 +02:00
} ] ,
} ) ;
2024-07-23 19:18:31 +02:00
await page . goto ( serverURL ) ;
2024-07-25 18:53:38 +02:00
await expect ( page . getByTestId ( 'message' ) ) . toHaveText ( 'Hello Alice, your certificate was issued by localhost!' ) ;
2024-07-12 11:42:24 +02:00
await page . close ( ) ;
} ) ;
2024-08-19 09:24:32 +02:00
test ( 'should pass with matching certificates when passing as content' , async ( { browser , startCCServer , asset , browserName } ) = > {
const serverURL = await startCCServer ( { useFakeLocalhost : browserName === 'webkit' && process . platform === 'darwin' } ) ;
const page = await browser . newPage ( {
ignoreHTTPSErrors : true ,
clientCertificates : [ {
origin : new URL ( serverURL ) . origin ,
cert : await fs . promises . readFile ( asset ( 'client-certificates/client/trusted/cert.pem' ) ) ,
key : await fs . promises . readFile ( asset ( 'client-certificates/client/trusted/key.pem' ) ) ,
} ] ,
} ) ;
await page . goto ( serverURL ) ;
await expect ( page . getByTestId ( 'message' ) ) . toHaveText ( 'Hello Alice, your certificate was issued by localhost!' ) ;
await page . close ( ) ;
} ) ;
2024-08-15 08:51:40 +02:00
test ( 'should not hang on tls errors during TLS 1.2 handshake' , async ( { browser , asset , platform , browserName } ) = > {
for ( const tlsVersion of [ 'TLSv1.3' , 'TLSv1.2' ] as const ) {
await test . step ( ` TLS version: ${ tlsVersion } ` , async ( ) = > {
const server = tls . createServer ( {
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 : true ,
minVersion : tlsVersion ,
maxVersion : tlsVersion ,
SNICallback : ( servername , cb ) = > {
// Always reject the connection by passing an error
cb ( new Error ( 'Connection rejected' ) , null ) ;
}
} , ( ) = > {
// Do nothing
} ) ;
const serverURL = await new Promise < string > ( resolve = > {
server . listen ( 0 , 'localhost' , ( ) = > {
const host = browserName === 'webkit' && platform === 'darwin' ? 'local.playwright' : 'localhost' ;
resolve ( ` https:// ${ host } : ${ ( server . address ( ) as net . AddressInfo ) . port } / ` ) ;
} ) ;
} ) ;
const page = await browser . newPage ( {
ignoreHTTPSErrors : true ,
clientCertificates : [ {
origin : new URL ( serverURL ) . origin ,
certPath : asset ( 'client-certificates/client/self-signed/cert.pem' ) ,
keyPath : asset ( 'client-certificates/client/self-signed/key.pem' ) ,
} ] ,
} ) ;
await page . goto ( serverURL ) ;
await expect ( page . getByText ( 'Playwright client-certificate error: Client network socket disconnected before secure TLS connection was established' ) ) . toBeVisible ( ) ;
await page . close ( ) ;
await new Promise < void > ( resolve = > server . close ( ( ) = > resolve ( ) ) ) ;
} ) ;
}
} ) ;
2024-08-05 10:54:33 +02:00
test ( 'should pass with matching certificates in pfx format' , async ( { browser , startCCServer , asset , browserName } ) = > {
const serverURL = await startCCServer ( { useFakeLocalhost : browserName === 'webkit' && process . platform === 'darwin' } ) ;
const page = await browser . newPage ( {
ignoreHTTPSErrors : true ,
clientCertificates : [ {
origin : new URL ( serverURL ) . origin ,
pfxPath : asset ( 'client-certificates/client/trusted/cert.pfx' ) ,
passphrase : 'secure'
} ] ,
} ) ;
await page . goto ( serverURL ) ;
await expect ( page . getByTestId ( 'message' ) ) . toHaveText ( 'Hello Alice, your certificate was issued by localhost!' ) ;
await page . close ( ) ;
} ) ;
2024-08-22 08:42:09 +02:00
test ( 'should handle TLS renegotiation with client certificates' , async ( { browser , asset , browserName , platform } ) = > {
const server : https.Server = 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 : false , // Initially don't request client cert
rejectUnauthorized : false ,
// TLSv1.3 does not support renegotiation
minVersion : 'TLSv1.2' ,
maxVersion : 'TLSv1.2' ,
} ) ;
server . on ( 'request' , async ( req , res ) = > {
if ( ! req . socket )
return ;
const renegotiate = ( ) = > new Promise < void > ( ( resolve , reject ) = > {
( req . socket as tls . TLSSocket ) . renegotiate ( {
requestCert : true ,
rejectUnauthorized : false
} , err = > {
if ( err )
reject ( err ) ;
else
resolve ( ) ;
} ) ;
} ) ;
if ( req . url === '/' ) {
res . writeHead ( 200 , { 'Content-Type' : 'text/html' , 'connection' : 'close' } ) ;
res . end ( ) ;
} else if ( req . url === '/from-fetch-api' ) {
res . writeHead ( 200 , {
'Content-Type' : 'text/plain' ,
'Transfer-Encoding' : 'chunked'
} ) ;
res . flushHeaders ( ) ;
await new Promise < void > ( resolve = > req . once ( 'data' , data = > {
res . write ( ` server received: ${ data . toString ( ) } \ n ` ) ;
resolve ( ) ;
} ) ) ;
await renegotiate ( ) ;
for ( let i = 0 ; i < 4 ; i ++ ) {
res . write ( ` ${ i } -from-server \ n ` ) ;
// Best-effort to trigger a new chunk
await new Promise < void > ( resolve = > setTimeout ( resolve , 100 ) ) ;
}
res . end ( 'server closed the connection' ) ;
} else if ( req . url === '/style.css' ) {
res . writeHead ( 200 , {
'Content-Type' : 'text/css' ,
'Content-Encoding' : 'gzip' ,
'Transfer-Encoding' : 'chunked'
} ) ;
await renegotiate ( ) ;
const stylesheet = `
button {
background - color : red ;
}
` ;
const stylesheetBuffer = await new Promise < Buffer > ( ( resolve , reject ) = > {
zlib . gzip ( stylesheet , ( err , buffer ) = > {
if ( err )
reject ( err ) ;
else
resolve ( buffer ) ;
} ) ;
} ) ;
for ( let i = 0 ; i < stylesheetBuffer . length ; i += 100 ) {
res . write ( stylesheetBuffer . slice ( i , i + 100 ) ) ;
// Best-effort to trigger a new chunk
await new Promise < void > ( resolve = > setTimeout ( resolve , 20 ) ) ;
}
res . end ( ) ;
} else {
res . writeHead ( 404 ) ;
res . end ( ) ;
}
} ) ;
await new Promise < void > ( resolve = > server . listen ( 0 , 'localhost' , resolve ) ) ;
2024-08-22 15:13:54 +02:00
const port = ( server . address ( ) as net . AddressInfo ) . port ;
2024-08-22 08:42:09 +02:00
const origin = 'https://' + ( browserName === 'webkit' && platform === 'darwin' ? 'local.playwright' : 'localhost' ) ;
const serverUrl = ` ${ origin } : ${ port } ` ;
const context = await browser . newContext ( {
ignoreHTTPSErrors : true ,
clientCertificates : [ {
origin ,
certPath : asset ( 'client-certificates/client/trusted/cert.pem' ) ,
keyPath : asset ( 'client-certificates/client/trusted/key.pem' ) ,
} ] ,
} ) ;
const page = await context . newPage ( ) ;
await test . step ( 'fetch API' , async ( ) = > {
await page . goto ( serverUrl ) ;
const response = await page . evaluate ( async ( ) = > {
const response = await fetch ( '/from-fetch-api' , {
method : 'POST' ,
body : 'client-request-payload'
} ) ;
return await response . text ( ) ;
} ) ;
expect ( response ) . toBe ( [
'server received: client-request-payload' ,
'0-from-server' ,
'1-from-server' ,
'2-from-server' ,
'3-from-server' ,
'server closed the connection'
] . join ( '\n' ) ) ;
} ) ;
await test . step ( 'Gzip encoded CSS Stylesheet' , async ( ) = > {
await page . goto ( serverUrl ) ;
// The <link> would throw with net::ERR_INVALID_CHUNKED_ENCODING
await page . setContent ( `
< button > Click me < / button >
< link rel = "stylesheet" href = "/style.css" >
` );
await expect ( page . locator ( 'button' ) ) . toHaveCSS ( 'background-color' , /* red */ 'rgb(255, 0, 0)' ) ;
} ) ;
await context . close ( ) ;
await new Promise < void > ( resolve = > server . close ( ( ) = > resolve ( ) ) ) ;
} ) ;
2024-08-19 09:24:32 +02:00
test ( 'should pass with matching certificates in pfx format when passing as content' , async ( { browser , startCCServer , asset , browserName } ) = > {
const serverURL = await startCCServer ( { useFakeLocalhost : browserName === 'webkit' && process . platform === 'darwin' } ) ;
const page = await browser . newPage ( {
ignoreHTTPSErrors : true ,
clientCertificates : [ {
origin : new URL ( serverURL ) . origin ,
pfx : await fs . promises . readFile ( asset ( 'client-certificates/client/trusted/cert.pfx' ) ) ,
passphrase : 'secure'
} ] ,
} ) ;
await page . goto ( serverURL ) ;
await expect ( page . getByTestId ( 'message' ) ) . toHaveText ( 'Hello Alice, your certificate was issued by localhost!' ) ;
await page . close ( ) ;
} ) ;
2024-08-05 14:42:29 +02:00
test ( 'should fail with matching certificates in legacy pfx format' , async ( { browser , startCCServer , asset , browserName } ) = > {
const serverURL = await startCCServer ( { useFakeLocalhost : browserName === 'webkit' && process . platform === 'darwin' } ) ;
2024-08-19 09:02:14 +02:00
await expect ( browser . newPage ( {
2024-08-05 14:42:29 +02:00
ignoreHTTPSErrors : true ,
clientCertificates : [ {
origin : new URL ( serverURL ) . origin ,
pfxPath : asset ( 'client-certificates/client/trusted/cert-legacy.pfx' ) ,
passphrase : 'secure'
} ] ,
2024-08-19 09:02:14 +02:00
} ) ) . rejects . toThrow ( 'Unsupported TLS certificate' ) ;
2024-08-05 14:42:29 +02:00
} ) ;
2024-08-05 10:54:33 +02:00
test ( 'should throw a http error if the pfx passphrase is incorect' , async ( { browser , startCCServer , asset , browserName } ) = > {
const serverURL = await startCCServer ( { useFakeLocalhost : browserName === 'webkit' && process . platform === 'darwin' } ) ;
2024-08-19 09:02:14 +02:00
await expect ( browser . newPage ( {
2024-08-05 10:54:33 +02:00
ignoreHTTPSErrors : true ,
clientCertificates : [ {
origin : new URL ( serverURL ) . origin ,
pfxPath : asset ( 'client-certificates/client/trusted/cert.pfx' ) ,
passphrase : 'this-password-is-incorrect'
} ] ,
2024-08-19 09:02:14 +02:00
} ) ) . rejects . toThrow ( 'Failed to load client certificate: mac verify failure' ) ;
2024-08-05 10:54:33 +02:00
} ) ;
2024-07-29 16:44:53 +02:00
test ( 'should pass with matching certificates on context APIRequestContext instance' , async ( { browser , startCCServer , asset , browserName } ) = > {
const serverURL = await startCCServer ( { host : '127.0.0.1' } ) ;
const baseOptions = {
certPath : asset ( 'client-certificates/client/trusted/cert.pem' ) ,
keyPath : asset ( 'client-certificates/client/trusted/key.pem' ) ,
} ;
const page = await browser . newPage ( {
clientCertificates : [ {
origin : new URL ( serverURL ) . origin ,
. . . baseOptions ,
} , {
origin : new URL ( serverURL ) . origin . replace ( 'localhost' , '127.0.0.1' ) ,
. . . baseOptions ,
} ] ,
} ) ;
for ( const url of [ serverURL , serverURL . replace ( 'localhost' , '127.0.0.1' ) ] ) {
const response = await page . request . get ( url ) ;
expect ( response . status ( ) ) . toBe ( 200 ) ;
expect ( await response . text ( ) ) . toContain ( 'Hello Alice, your certificate was issued by localhost!' ) ;
}
await page . close ( ) ;
} ) ;
2024-07-24 19:13:03 +02:00
test ( 'should pass with matching certificates and trailing slash' , async ( { browser , startCCServer , asset , browserName } ) = > {
const serverURL = await startCCServer ( { useFakeLocalhost : browserName === 'webkit' && process . platform === 'darwin' } ) ;
const page = await browser . newPage ( {
2024-08-02 08:34:28 +02:00
ignoreHTTPSErrors : true ,
2024-07-24 19:13:03 +02:00
clientCertificates : [ {
origin : serverURL ,
certPath : asset ( 'client-certificates/client/trusted/cert.pem' ) ,
keyPath : asset ( 'client-certificates/client/trusted/key.pem' ) ,
} ] ,
} ) ;
await page . goto ( serverURL ) ;
await expect ( page . getByText ( 'Hello Alice, your certificate was issued by localhost!' ) ) . toBeVisible ( ) ;
await page . close ( ) ;
} ) ;
2024-07-19 12:04:12 +02:00
test ( 'should have ignoreHTTPSErrors=false by default' , async ( { browser , httpsServer , asset , browserName , platform } ) = > {
2024-07-18 22:37:11 +02:00
const page = await browser . newPage ( {
clientCertificates : [ {
2024-07-23 22:56:36 +02:00
origin : 'https://just-there-that-the-client-certificates-proxy-server-is-getting-launched.com' ,
certPath : asset ( 'client-certificates/client/trusted/cert.pem' ) ,
keyPath : asset ( 'client-certificates/client/trusted/key.pem' ) ,
2024-07-18 22:37:11 +02:00
} ] ,
} ) ;
2024-07-19 12:04:12 +02:00
await page . goto ( browserName === 'webkit' && platform === 'darwin' ? httpsServer . EMPTY_PAGE . replace ( 'localhost' , 'local.playwright' ) : httpsServer . EMPTY_PAGE ) ;
2024-07-29 14:39:14 +02:00
await expect ( page . getByText ( 'Playwright client-certificate error: self-signed certificate' ) ) . toBeVisible ( ) ;
2024-07-18 22:37:11 +02:00
await page . close ( ) ;
} ) ;
2024-07-24 11:39:39 +02:00
test ( 'support http2' , async ( { browser , startCCServer , asset , browserName } ) = > {
test . skip ( browserName === 'webkit' && process . platform === 'darwin' , 'WebKit on macOS doesn\n proxy localhost' ) ;
2024-07-26 11:28:45 +02:00
const enableHTTP1FallbackWhenUsingHttp2 = browserName === 'webkit' && process . platform === 'linux' ;
const serverURL = await startCCServer ( { http2 : true , enableHTTP1FallbackWhenUsingHttp2 } ) ;
2024-07-24 11:39:39 +02:00
const page = await browser . newPage ( {
2024-08-02 08:34:28 +02:00
ignoreHTTPSErrors : true ,
2024-07-24 11:39:39 +02:00
clientCertificates : [ {
origin : new URL ( serverURL ) . origin ,
certPath : asset ( 'client-certificates/client/trusted/cert.pem' ) ,
keyPath : asset ( 'client-certificates/client/trusted/key.pem' ) ,
} ] ,
} ) ;
// TODO: We should investigate why http2 is not supported in WebKit on Linux.
// https://bugs.webkit.org/show_bug.cgi?id=276990
2024-07-26 11:28:45 +02:00
const expectedProtocol = enableHTTP1FallbackWhenUsingHttp2 ? 'http/1.1' : 'h2' ;
2024-07-24 11:39:39 +02:00
{
await page . goto ( serverURL . replace ( 'localhost' , 'local.playwright' ) ) ;
2024-07-25 18:53:38 +02:00
await expect ( page . getByTestId ( 'message' ) ) . toHaveText ( 'Sorry, but you need to provide a client certificate to continue.' ) ;
await expect ( page . getByTestId ( 'alpn-protocol' ) ) . toHaveText ( expectedProtocol ) ;
await expect ( page . getByTestId ( 'servername' ) ) . toHaveText ( 'local.playwright' ) ;
2024-07-24 11:39:39 +02:00
}
{
await page . goto ( serverURL ) ;
2024-07-25 18:53:38 +02:00
await expect ( page . getByTestId ( 'message' ) ) . toHaveText ( 'Hello Alice, your certificate was issued by localhost!' ) ;
await expect ( page . getByTestId ( 'alpn-protocol' ) ) . toHaveText ( expectedProtocol ) ;
2024-07-24 11:39:39 +02:00
}
await page . close ( ) ;
} ) ;
test ( 'support http2 if the browser only supports http1.1' , async ( { browserType , browserName , startCCServer , asset } ) = > {
test . skip ( browserName !== 'chromium' ) ;
2024-07-26 11:28:45 +02:00
const serverURL = await startCCServer ( { http2 : true , enableHTTP1FallbackWhenUsingHttp2 : true } ) ;
2024-07-24 11:39:39 +02:00
const browser = await browserType . launch ( { args : [ '--disable-http2' ] } ) ;
const page = await browser . newPage ( {
2024-08-02 08:34:28 +02:00
ignoreHTTPSErrors : true ,
2024-07-24 11:39:39 +02:00
clientCertificates : [ {
origin : new URL ( serverURL ) . origin ,
certPath : asset ( 'client-certificates/client/trusted/cert.pem' ) ,
keyPath : asset ( 'client-certificates/client/trusted/key.pem' ) ,
} ] ,
} ) ;
{
await page . goto ( serverURL . replace ( 'localhost' , 'local.playwright' ) ) ;
2024-07-25 18:53:38 +02:00
await expect ( page . getByTestId ( 'message' ) ) . toHaveText ( 'Sorry, but you need to provide a client certificate to continue.' ) ;
await expect ( page . getByTestId ( 'alpn-protocol' ) ) . toHaveText ( 'http/1.1' ) ;
2024-07-24 11:39:39 +02:00
}
{
await page . goto ( serverURL ) ;
2024-07-25 18:53:38 +02:00
await expect ( page . getByTestId ( 'message' ) ) . toHaveText ( 'Hello Alice, your certificate was issued by localhost!' ) ;
await expect ( page . getByTestId ( 'alpn-protocol' ) ) . toHaveText ( 'http/1.1' ) ;
2024-07-24 11:39:39 +02:00
}
await browser . close ( ) ;
} ) ;
2024-07-26 11:28:45 +02:00
test ( 'should return target connection errors when using http2' , async ( { browser , startCCServer , asset , browserName } ) = > {
test . skip ( browserName === 'webkit' && process . platform === 'darwin' , 'WebKit on macOS doesn\n proxy localhost' ) ;
test . fixme ( browserName === 'webkit' && process . platform === 'linux' , 'WebKit on Linux does not support http2 https://bugs.webkit.org/show_bug.cgi?id=276990' ) ;
test . skip ( + process . versions . node . split ( '.' ) [ 0 ] < 20 , 'http2.performServerHandshake is not supported in older Node.js versions' ) ;
const serverURL = await startCCServer ( { http2 : true } ) ;
const page = await browser . newPage ( {
clientCertificates : [ {
origin : 'https://just-there-that-the-client-certificates-proxy-server-is-getting-launched.com' ,
certPath : asset ( 'client-certificates/client/trusted/cert.pem' ) ,
keyPath : asset ( 'client-certificates/client/trusted/key.pem' ) ,
} ] ,
} ) ;
await page . goto ( serverURL ) ;
await expect ( page . getByText ( 'Playwright client-certificate error: self-signed certificate' ) ) . toBeVisible ( ) ;
await page . close ( ) ;
} ) ;
2024-08-22 15:13:54 +02:00
test ( 'should handle rejected certificate in handshake with HTTP/2' , async ( { browser , asset , browserName , platform } ) = > {
const server : http2.Http2SecureServer = createHttp2Server ( {
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 ,
} , async ( req : http2.Http2ServerRequest , res : http2.Http2ServerResponse ) = > {
res . writeHead ( 200 , { 'Content-Type' : 'text/html' } ) ;
res . end ( 'Hello world' ) ;
} ) ;
await new Promise < void > ( resolve = > server . listen ( 0 , 'localhost' , resolve ) ) ;
const port = ( server . address ( ) as net . AddressInfo ) . port ;
const serverUrl = 'https://' + ( browserName === 'webkit' && platform === 'darwin' ? 'local.playwright' : 'localhost' ) + ':' + port ;
const context = await browser . newContext ( {
ignoreHTTPSErrors : true ,
clientCertificates : [ {
origin : 'https://just-there-that-the-client-certificates-proxy-server-is-getting-launched.com' ,
certPath : asset ( 'client-certificates/client/trusted/cert.pem' ) ,
keyPath : asset ( 'client-certificates/client/trusted/key.pem' ) ,
} ] ,
} ) ;
const page = await context . newPage ( ) ;
// This was triggering an unhandled error before.
await page . goto ( serverUrl ) . catch ( ( ) = > { } ) ;
await context . close ( ) ;
await new Promise < void > ( resolve = > server . close ( ( ) = > resolve ( ) ) ) ;
} ) ;
2024-07-12 11:42:24 +02:00
test . describe ( 'persistentContext' , ( ) = > {
test ( 'validate input' , async ( { launchPersistent } ) = > {
test . slow ( ) ;
for ( const [ contextOptions , expected ] of kValidationSubTests )
await expect ( launchPersistent ( contextOptions ) ) . rejects . toThrow ( expected ) ;
} ) ;
2024-07-23 19:18:31 +02:00
test ( 'should pass with matching certificates' , async ( { launchPersistent , startCCServer , asset , browserName } ) = > {
const serverURL = await startCCServer ( { useFakeLocalhost : browserName === 'webkit' && process . platform === 'darwin' } ) ;
2024-07-12 11:42:24 +02:00
const { page } = await launchPersistent ( {
2024-08-02 08:34:28 +02:00
ignoreHTTPSErrors : true ,
2024-07-12 11:42:24 +02:00
clientCertificates : [ {
2024-07-23 22:56:36 +02:00
origin : new URL ( serverURL ) . origin ,
certPath : asset ( 'client-certificates/client/trusted/cert.pem' ) ,
keyPath : asset ( 'client-certificates/client/trusted/key.pem' ) ,
2024-07-12 11:42:24 +02:00
} ] ,
} ) ;
2024-07-23 19:18:31 +02:00
await page . goto ( serverURL ) ;
2024-07-25 18:53:38 +02:00
await expect ( page . getByTestId ( 'message' ) ) . toHaveText ( 'Hello Alice, your certificate was issued by localhost!' ) ;
2024-07-12 11:42:24 +02:00
} ) ;
} ) ;
} ) ;