mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00:00 
			
		
		
		
	feat(containers): implement reverse proxy for novnc and server (#18569)
This patch adds a reverse proxy in front of novnc and playwright server inside the container. As a result: - container exposes a single HTTP port to the host - all Playwright clients can connect using this exposed port, e.g. `http://127.0.0.1:5400` - navigating to the exposed port in the browser lands on a nice HTML page with a link to view container screen - users can also manually navigate to `http://127.0.0.1:5400/screen` to view screen
This commit is contained in:
		
							parent
							
								
									526bc3f252
								
							
						
					
					
						commit
						858c2453b3
					
				
							
								
								
									
										35
									
								
								packages/playwright-core/bin/container_landing.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								packages/playwright-core/bin/container_landing.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | |||||||
|  | <!DOCTYPE HTML> | ||||||
|  | <style> | ||||||
|  |   :root { | ||||||
|  |     --monospace: Consolas, Menlo, monospace; | ||||||
|  |     --regular: system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Droid Sans, Helvetica Neue, Fira Sans, sans-serif; | ||||||
|  |     --text-color: #24292e; | ||||||
|  |     --background-color: #fff; | ||||||
|  |     --link-color: #0366d6; | ||||||
|  |     --border-color: #f3e5f5; | ||||||
|  |   } | ||||||
|  |   body { | ||||||
|  |     margin: 0; | ||||||
|  |     font: 1em/1.6 var(--regular); | ||||||
|  |     font-weight: 400; | ||||||
|  |     font-style: normal; | ||||||
|  |     text-rendering: optimizeLegibility; | ||||||
|  |     -webkit-font-smoothing: antialiased; | ||||||
|  |     color: var(--text-color); | ||||||
|  |     font-size: 12px; | ||||||
|  |   } | ||||||
|  |   img, iframe { border: none } | ||||||
|  |   h1,h2,h3,h4,h5,h6 {margin: 0.6em 0; font-weight: normal} | ||||||
|  |   h1 {font-size: 2.625em; line-height: 1.2} | ||||||
|  |   h2 {font-size: 1.625em; line-height: 1.2} | ||||||
|  |   h3 {font-size: 1.3125em;line-height: 1.24} | ||||||
|  |   h4 {font-size: 1.1875em;line-height: 1.23} | ||||||
|  |   h5,h6 {font-size: 1em; font-weight:bold} | ||||||
|  |   a { color: var(--link-color); text-decoration: none; } | ||||||
|  |   hbox { display: flex; align-items: center; } | ||||||
|  |   vbox { display: flex; flex-direction: column; } | ||||||
|  | </style> | ||||||
|  | <body style='display: flex; align-items: center; justify-content: center; flex-direction: column;'> | ||||||
|  |   <h1>Playwright Container</h1> | ||||||
|  |   <a href="/screen">View Screen</a> | ||||||
|  | </body> | ||||||
							
								
								
									
										69
									
								
								packages/playwright-core/bin/container_novnc_proxy.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								packages/playwright-core/bin/container_novnc_proxy.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,69 @@ | |||||||
|  | /** | ||||||
|  |  * 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 http = require('http'); | ||||||
|  | const fs = require('fs'); | ||||||
|  | const path = require('path'); | ||||||
|  | const { debug, program } = require('../lib/utilsBundle'); | ||||||
|  | const { ProxyServer } = require('../lib/third_party/http_proxy'); | ||||||
|  | 
 | ||||||
|  | const debugLog = debug('pw:proxy'); | ||||||
|  | 
 | ||||||
|  | program | ||||||
|  |     .command('start') | ||||||
|  |     .description('reverse proxy for novnc and playwright server') | ||||||
|  |     .option('--port <number>', 'port number') | ||||||
|  |     .option('--server-endpoint <url>', 'Playwright Server endpoint') | ||||||
|  |     .option('--novnc-endpoint <url>', 'novnc server endpoint') | ||||||
|  |     .option('--novnc-ws-path <string>', 'novnc websocket path') | ||||||
|  |     .action(async function(options) { | ||||||
|  |       launchReverseProxy(options.port, options.serverEndpoint, options.novncEndpoint, options.novncWsPath); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  | program.parse(process.argv); | ||||||
|  | 
 | ||||||
|  | async function launchReverseProxy(port, serverEndpoint, novncEndpoint, novncWSPath) { | ||||||
|  |   const vncProxy = new ProxyServer(novncEndpoint, debugLog); | ||||||
|  |   const serverProxy = new ProxyServer(serverEndpoint, debugLog); | ||||||
|  | 
 | ||||||
|  |   const httpServer = http.createServer((request, response) => { | ||||||
|  |     if (request.url === '/' && request.method === 'GET') { | ||||||
|  |       response.writeHead(200, { | ||||||
|  |         'content-type': 'text/html', | ||||||
|  |       }).end(fs.readFileSync(path.join(__dirname, 'container_landing.html'), 'utf-8')); | ||||||
|  |     } else if ((request.url === '/screen' || request.url === '/screen/') && request.method === 'GET') { | ||||||
|  |       response.writeHead(307, { | ||||||
|  |         Location: `/screen/?resize=scale&autoconnect=1&path=${novncWSPath}`, | ||||||
|  |       }).end(); | ||||||
|  |     } else if (request.url?.startsWith('/screen')) { | ||||||
|  |       request.url = request.url.substring('/screen'.length); | ||||||
|  |       vncProxy.web(request, response); | ||||||
|  |     } else { | ||||||
|  |       serverProxy.web(request, response); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |   httpServer.on('error', error => debugLog(error)); | ||||||
|  |   httpServer.on('upgrade', (request, socket, head) => { | ||||||
|  |     if (request.url === '/' + novncWSPath) | ||||||
|  |       vncProxy.ws(request, socket, head); | ||||||
|  |     else | ||||||
|  |       serverProxy.ws(request, socket, head); | ||||||
|  |   }); | ||||||
|  |   httpServer.listen(port, () => { | ||||||
|  |     console.log('Playwright container listening on', port); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| @ -1,5 +1,9 @@ | |||||||
| #!/bin/bash | #!/bin/bash | ||||||
| set -e | set -e | ||||||
|  | 
 | ||||||
|  | trap "cd $(pwd -P)" EXIT | ||||||
|  | cd "$(dirname "$0")" | ||||||
|  | 
 | ||||||
| SCREEN_WIDTH=1360 | SCREEN_WIDTH=1360 | ||||||
| SCREEN_HEIGHT=1020 | SCREEN_HEIGHT=1020 | ||||||
| SCREEN_DEPTH=24 | SCREEN_DEPTH=24 | ||||||
| @ -19,20 +23,22 @@ for i in $(seq 1 500); do | |||||||
|   sleep 0.2 |   sleep 0.2 | ||||||
| done | done | ||||||
| 
 | 
 | ||||||
|  | # Launch x11 | ||||||
| nohup x11vnc -noprimary -nosetprimary -forever -shared -rfbport 5900 -rfbportv6 5900 -display "$DISPLAY" >/dev/null 2>&1 & | nohup x11vnc -noprimary -nosetprimary -forever -shared -rfbport 5900 -rfbportv6 5900 -display "$DISPLAY" >/dev/null 2>&1 & | ||||||
|  | # Launch novnc | ||||||
| nohup /opt/bin/noVNC/utils/novnc_proxy --listen 7900 --vnc localhost:5900 >/dev/null 2>&1 & | nohup /opt/bin/noVNC/utils/novnc_proxy --listen 7900 --vnc localhost:5900 >/dev/null 2>&1 & | ||||||
|  | # Launch reverse proxy | ||||||
|  | NOVNC_UUID=$(cat /proc/sys/kernel/random/uuid) | ||||||
|  | node ./container_novnc_proxy.js start --server-endpoint="http://127.0.0.1:5200" --novnc-endpoint="http://127.0.0.1:7900" --novnc-ws-path="${NOVNC_UUID}" --port 5400 & | ||||||
| 
 | 
 | ||||||
| cd /ms-playwright-agent | cd /ms-playwright-agent | ||||||
| 
 | 
 | ||||||
| NOVNC_UUID=$(cat /proc/sys/kernel/random/uuid) |  | ||||||
| echo "novnc is listening on http://127.0.0.1:7900?path=$NOVNC_UUID&resize=scale&autoconnect=1" |  | ||||||
| 
 |  | ||||||
| PW_UUID=$(cat /proc/sys/kernel/random/uuid) | PW_UUID=$(cat /proc/sys/kernel/random/uuid) | ||||||
| 
 | 
 | ||||||
| # Make sure to re-start playwright server if something goes wrong. | # Make sure to re-start playwright server if something goes wrong. | ||||||
| # The approach taken from: https://stackoverflow.com/a/697064/314883 | # The approach taken from: https://stackoverflow.com/a/697064/314883 | ||||||
| 
 | 
 | ||||||
| until npx playwright run-server --port=5400 --path=/$PW_UUID --proxy-mode=tether; do | until npx playwright run-server --port=5200 --path=/$PW_UUID --proxy-mode=tether; do | ||||||
|   echo "Server crashed with exit code $?. Respawning.." >&2 |   echo "Server crashed with exit code $?. Respawning.." >&2 | ||||||
|   sleep 1 |   sleep 1 | ||||||
| done | done | ||||||
|  | |||||||
| @ -42,13 +42,8 @@ async function startPlaywrightContainer(port: number) { | |||||||
|   const info = await ensurePlaywrightContainerOrDie(port); |   const info = await ensurePlaywrightContainerOrDie(port); | ||||||
|   const deltaMs = (Date.now() - time); |   const deltaMs = (Date.now() - time); | ||||||
|   console.log('Done in ' + (deltaMs / 1000).toFixed(1) + 's'); |   console.log('Done in ' + (deltaMs / 1000).toFixed(1) + 's'); | ||||||
|   await tetherHostNetwork(info.wsEndpoint); |   await tetherHostNetwork(info.httpEndpoint); | ||||||
| 
 |   console.log('Endpoint:', info.httpEndpoint); | ||||||
|   console.log([ |  | ||||||
|     `- Endpoint: ${info.httpEndpoint}`, |  | ||||||
|     `- View screen:`, |  | ||||||
|     `    ${info.vncSession}`, |  | ||||||
|   ].join('\n')); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async function stopAllPlaywrightContainers() { | async function stopAllPlaywrightContainers() { | ||||||
| @ -133,8 +128,6 @@ async function buildPlaywrightImage() { | |||||||
| 
 | 
 | ||||||
| interface ContainerInfo { | interface ContainerInfo { | ||||||
|   httpEndpoint: string; |   httpEndpoint: string; | ||||||
|   wsEndpoint: string; |  | ||||||
|   vncSession: string; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async function printDockerStatus() { | async function printDockerStatus() { | ||||||
| @ -145,8 +138,7 @@ async function printDockerStatus() { | |||||||
|     dockerEngineRunning: isDockerEngine, |     dockerEngineRunning: isDockerEngine, | ||||||
|     imageName: VRT_IMAGE_NAME, |     imageName: VRT_IMAGE_NAME, | ||||||
|     imageIsPulled, |     imageIsPulled, | ||||||
|     containerWSEndpoint: info?.wsEndpoint ?? '', |     containerEndpoint: info?.httpEndpoint ?? '', | ||||||
|     containerVNCEndpoint: info?.vncSession ?? '', |  | ||||||
|   }, null, 2)); |   }, null, 2)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -170,18 +162,13 @@ export async function containerInfo(): Promise<ContainerInfo|undefined> { | |||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const WS_LINE_PREFIX = 'Listening on ws://'; |   const WS_LINE_PREFIX = 'Listening on ws://'; | ||||||
|  |   const REVERSE_PROXY_LINE_PREFIX = 'Playwright container listening on'; | ||||||
|   const webSocketLine = logLines.find(line => line.startsWith(WS_LINE_PREFIX)); |   const webSocketLine = logLines.find(line => line.startsWith(WS_LINE_PREFIX)); | ||||||
|   const NOVNC_LINE_PREFIX = 'novnc is listening on '; |   const reverseProxyLine = logLines.find(line => line.startsWith(REVERSE_PROXY_LINE_PREFIX)); | ||||||
|   const novncLine = logLines.find(line => line.startsWith(NOVNC_LINE_PREFIX)); |   if (!webSocketLine || !reverseProxyLine) | ||||||
|   if (!novncLine || !webSocketLine) |  | ||||||
|     return undefined; |     return undefined; | ||||||
|   const wsEndpoint = containerUrlToHostUrl('ws://' + webSocketLine.substring(WS_LINE_PREFIX.length)); |   const httpEndpoint = containerUrlToHostUrl('http://127.0.0.1:' + reverseProxyLine.substring(REVERSE_PROXY_LINE_PREFIX.length).trim()); | ||||||
|   const vncSession = containerUrlToHostUrl(novncLine.substring(NOVNC_LINE_PREFIX.length)); |   return httpEndpoint ? { httpEndpoint } : undefined; | ||||||
|   if (!wsEndpoint || !vncSession) |  | ||||||
|     return undefined; |  | ||||||
|   const wsUrl = new URL(wsEndpoint); |  | ||||||
|   const httpEndpoint = 'http://' + wsUrl.host; |  | ||||||
|   return { wsEndpoint, vncSession, httpEndpoint }; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function ensurePlaywrightContainerOrDie(port: number): Promise<ContainerInfo> { | export async function ensurePlaywrightContainerOrDie(port: number): Promise<ContainerInfo> { | ||||||
| @ -249,7 +236,6 @@ export async function ensurePlaywrightContainerOrDie(port: number): Promise<Cont | |||||||
|     autoRemove: true, |     autoRemove: true, | ||||||
|     ports: [ |     ports: [ | ||||||
|       { container: 5400, host: port }, |       { container: 5400, host: port }, | ||||||
|       { container: 7900, host: 0 }, |  | ||||||
|     ], |     ], | ||||||
|     labels: { |     labels: { | ||||||
|       [VRT_CONTAINER_LABEL_NAME]: VRT_CONTAINER_LABEL_VALUE, |       [VRT_CONTAINER_LABEL_NAME]: VRT_CONTAINER_LABEL_VALUE, | ||||||
|  | |||||||
							
								
								
									
										165
									
								
								packages/playwright-core/src/third_party/http_proxy.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								packages/playwright-core/src/third_party/http_proxy.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,165 @@ | |||||||
|  | /** | ||||||
|  |  * node-http-proxy | ||||||
|  |  * | ||||||
|  |  * Copyright (c) 2010-2016 Charlie Robbins, Jarrett Cruger & the Contributors. | ||||||
|  |  * Modifications copyright (c) Microsoft Corporation. | ||||||
|  |  * | ||||||
|  |  * Permission is hereby granted, free of charge, to any person obtaining | ||||||
|  |  * a copy of this software and associated documentation files (the | ||||||
|  |  * "Software"), to deal in the Software without restriction, including | ||||||
|  |  * without limitation the rights to use, copy, modify, merge, publish, | ||||||
|  |  * distribute, sublicense, and/or sell copies of the Software, and to | ||||||
|  |  * permit persons to whom the Software is furnished to do so, subject to | ||||||
|  |  * the following conditions: | ||||||
|  |  * | ||||||
|  |  * The above copyright notice and this permission notice shall be | ||||||
|  |  * included in all copies or substantial portions of the Software. | ||||||
|  |  * | ||||||
|  |  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||||||
|  |  * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | ||||||
|  |  * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | ||||||
|  |  * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | ||||||
|  |  * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | ||||||
|  |  * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | ||||||
|  |  * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||||||
|  |  */ | ||||||
|  | const URL = require('url'); | ||||||
|  | const http = require('http'); | ||||||
|  | const https = require('https'); | ||||||
|  | 
 | ||||||
|  | const upgradeHeader = /(^|,)\s*upgrade\s*($|,)/i; | ||||||
|  | 
 | ||||||
|  | // This is a stripped-down version of
 | ||||||
|  | // https://github.com/http-party/node-http-proxy
 | ||||||
|  | // library that implements a basic reverse proxy
 | ||||||
|  | // for both HTTP and WS connections.
 | ||||||
|  | class ProxyServer { | ||||||
|  |   constructor(target, log = () => {}) { | ||||||
|  |     this._target = URL.parse(target); | ||||||
|  |     this._log = log; | ||||||
|  |     if (this._target.path !== '/') | ||||||
|  |       throw new Error('ERROR: target must have no path'); | ||||||
|  |     this._agent = this._target.protocol === 'https:' ? https : http; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   web(req, res) { | ||||||
|  |    if ((req.method === 'DELETE' || req.method === 'OPTIONS') && !req.headers['content-length']) { | ||||||
|  |       req.headers['content-length'] = '0'; | ||||||
|  |       delete req.headers['transfer-encoding']; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Request initalization
 | ||||||
|  |     const options = { | ||||||
|  |       protocol: this._target.protocol, | ||||||
|  |       hostname: this._target.hostname, | ||||||
|  |       port: this._target.port, | ||||||
|  |       path: req.url, | ||||||
|  |       method: req.method, | ||||||
|  |       headers: req.headers, | ||||||
|  |     }; | ||||||
|  |     if (typeof options.headers.connection !== 'string' || !upgradeHeader.test(options.headers.connection)) | ||||||
|  |       options.headers.connection = 'close'; | ||||||
|  |     const proxyReq = this._agent.request(options); | ||||||
|  | 
 | ||||||
|  |     req.on('aborted', () => proxyReq.abort()); | ||||||
|  | 
 | ||||||
|  |     const errorHandler = error => { | ||||||
|  |       this._log(error); | ||||||
|  |       if (req.socket.destroyed && err.code === 'ECONNRESET') | ||||||
|  |         return proxyReq.abort(); | ||||||
|  |     } | ||||||
|  |     req.on('error', errorHandler); | ||||||
|  |     proxyReq.on('error', errorHandler); | ||||||
|  | 
 | ||||||
|  |     req.pipe(proxyReq); | ||||||
|  | 
 | ||||||
|  |     proxyReq.on('response', proxyRes => { | ||||||
|  |       if (!res.headersSent) { | ||||||
|  |         if (req.httpVersion !== '2.0' && !proxyRes.headers.connection) | ||||||
|  |           proxyRes.headers.connection = req.headers.connection || 'keep-alive'; | ||||||
|  |         for (const [key, value] of Object.entries(proxyRes.headers)) { | ||||||
|  |           if (value !== undefined) | ||||||
|  |             res.setHeader(String(key).trim(), value); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         res.statusCode = proxyRes.statusCode; | ||||||
|  |         if (proxyRes.statusMessage) | ||||||
|  |           res.statusMessage = proxyRes.statusMessage; | ||||||
|  |       } | ||||||
|  |       if (!res.finished) | ||||||
|  |         proxyRes.pipe(res); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   ws(req, socket, head) { | ||||||
|  |     if (req.method !== 'GET' || !req.headers.upgrade || req.headers.upgrade.toLowerCase() !== 'websocket') { | ||||||
|  |       socket.destroy(); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     socket.setTimeout(0); | ||||||
|  |     socket.setNoDelay(true); | ||||||
|  |     socket.setKeepAlive(true, 0); | ||||||
|  | 
 | ||||||
|  |     if (head && head.length) | ||||||
|  |       socket.unshift(head); | ||||||
|  | 
 | ||||||
|  |     const proxyReq = this._agent.request({ | ||||||
|  |       protocol: this._target.protocol, | ||||||
|  |       hostname: this._target.hostname, | ||||||
|  |       port: this._target.port, | ||||||
|  |       path: req.url, | ||||||
|  |       method: req.method, | ||||||
|  |       headers: req.headers, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     // Error Handler
 | ||||||
|  |     const errorHandler = err => { | ||||||
|  |       this._log(err); | ||||||
|  |       socket.end(); | ||||||
|  |     } | ||||||
|  |     socket.on('error', errorHandler); | ||||||
|  |     proxyReq.on('error', errorHandler); | ||||||
|  |     proxyReq.on('response', function (res) { | ||||||
|  |       // if upgrade event isn't going to happen, close the socket
 | ||||||
|  |       if (!res.upgrade) { | ||||||
|  |         socket.write(createHTTPHeader('HTTP/' + res.httpVersion + ' ' + res.statusCode + ' ' + res.statusMessage, res.headers)); | ||||||
|  |         res.pipe(socket); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |     proxyReq.on('upgrade', function(proxyRes, proxySocket, proxyHead) { | ||||||
|  |       proxySocket.on('error', errorHandler); | ||||||
|  | 
 | ||||||
|  |       // The pipe below will end proxySocket if socket closes cleanly, but not
 | ||||||
|  |       // if it errors (eg, vanishes from the net and starts returning
 | ||||||
|  |       // EHOSTUNREACH). We need to do that explicitly.
 | ||||||
|  |       socket.on('error', () => proxySocket.end()); | ||||||
|  | 
 | ||||||
|  |       proxySocket.setTimeout(0); | ||||||
|  |       proxySocket.setNoDelay(true); | ||||||
|  |       proxySocket.setKeepAlive(true, 0); | ||||||
|  | 
 | ||||||
|  |       if (proxyHead && proxyHead.length) | ||||||
|  |         proxySocket.unshift(proxyHead); | ||||||
|  | 
 | ||||||
|  |       //
 | ||||||
|  |       // Remark: Handle writing the headers to the socket when switching protocols
 | ||||||
|  |       // Also handles when a header is an array
 | ||||||
|  |       //
 | ||||||
|  |       socket.write(createHTTPHeader('HTTP/1.1 101 Switching Protocols', proxyRes.headers)); | ||||||
|  |       proxySocket.pipe(socket).pipe(proxySocket); | ||||||
|  |     }); | ||||||
|  |     return proxyReq.end(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function createHTTPHeader(line, headers) { | ||||||
|  |   const lines = [line]; | ||||||
|  |   for (const [key, arrayOrValue] of Object.entries(headers)) { | ||||||
|  |     for (const value of [arrayOrValue].flat()) | ||||||
|  |       lines.push(key + ': ' + value); | ||||||
|  |   } | ||||||
|  |   return lines.join('\r\n') + '\r\n\r\n'; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = { ProxyServer }; | ||||||
| @ -33,9 +33,8 @@ export const dockerPlugin: TestRunnerPlugin = { | |||||||
|     const info = await containerInfo(); |     const info = await containerInfo(); | ||||||
|     if (!info) |     if (!info) | ||||||
|       throw new Error('ERROR: please launch docker container separately!'); |       throw new Error('ERROR: please launch docker container separately!'); | ||||||
|     println(colors.dim(`View screen: ${info.vncSession}`)); |  | ||||||
|     println(''); |     println(''); | ||||||
|     process.env.PW_TEST_CONNECT_WS_ENDPOINT = info.wsEndpoint; |     process.env.PW_TEST_CONNECT_WS_ENDPOINT = info.httpEndpoint; | ||||||
|     process.env.PW_TEST_CONNECT_HEADERS = JSON.stringify({ |     process.env.PW_TEST_CONNECT_HEADERS = JSON.stringify({ | ||||||
|       'x-playwright-proxy': '*', |       'x-playwright-proxy': '*', | ||||||
|     }); |     }); | ||||||
|  | |||||||
| @ -48,7 +48,7 @@ test.describe('installed image', () => { | |||||||
|       shell: true, |       shell: true, | ||||||
|       cwd: path.join(__dirname, '..', '..'), |       cwd: path.join(__dirname, '..', '..'), | ||||||
|     }); |     }); | ||||||
|     await dockerProcess.waitForOutput('- Endpoint:'); |     await dockerProcess.waitForOutput('Endpoint:'); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   test.afterAll(async ({ exec }) => { |   test.afterAll(async ({ exec }) => { | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Andrey Lushnikov
						Andrey Lushnikov