From 0f040df83b22d625b980e5ccbc0c705d08dc0416 Mon Sep 17 00:00:00 2001 From: Max Rozen <3822106+rozenmd@users.noreply.github.com> Date: Tue, 27 Sep 2022 10:47:32 +0200 Subject: [PATCH 1/9] fix: pass protocols from headers --- packages/wrangler/src/proxy.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/wrangler/src/proxy.ts b/packages/wrangler/src/proxy.ts index bc71aaa7856..bf2c724dbd3 100644 --- a/packages/wrangler/src/proxy.ts +++ b/packages/wrangler/src/proxy.ts @@ -280,11 +280,17 @@ export function usePreviewServer({ const { headers, url } = message; addCfPreviewTokenHeader(headers, previewToken.value); headers["host"] = previewToken.host; - const localWebsocket = new WebSocket(message, socket, body) as IWebsocket; - // TODO(soon): Custom WebSocket protocol is not working? + const protocols = headers["sec-websocket-protocol"]; + const localWebsocket = new WebSocket( + message, + socket, + body, + protocols + ) as IWebsocket; + const remoteWebsocketClient = new WebSocket.Client( `wss://${previewToken.host}${url}`, - [], + protocols, { headers } ) as IWebsocket; localWebsocket.pipe(remoteWebsocketClient).pipe(localWebsocket); From 8b5c584ba12237bdec2be1e4ec0ab6f4900e6359 Mon Sep 17 00:00:00 2001 From: Max Rozen <3822106+rozenmd@users.noreply.github.com> Date: Tue, 27 Sep 2022 10:52:10 +0200 Subject: [PATCH 2/9] Create tall-radios-report.md --- .changeset/tall-radios-report.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/tall-radios-report.md diff --git a/.changeset/tall-radios-report.md b/.changeset/tall-radios-report.md new file mode 100644 index 00000000000..b868d9e3a5c --- /dev/null +++ b/.changeset/tall-radios-report.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +fix: pass protocols from headers for wrangler dev + +Prior to this change, the protocol passed between the client and the worker was being stripped out by wrangler. From 8bb3b3d71f6bda9d33c9080eb3da88bdf377804a Mon Sep 17 00:00:00 2001 From: Max Rozen <3822106+rozenmd@users.noreply.github.com> Date: Tue, 27 Sep 2022 11:04:22 +0200 Subject: [PATCH 3/9] log protocols --- packages/wrangler/src/proxy.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/wrangler/src/proxy.ts b/packages/wrangler/src/proxy.ts index bf2c724dbd3..c5244b5aaae 100644 --- a/packages/wrangler/src/proxy.ts +++ b/packages/wrangler/src/proxy.ts @@ -281,6 +281,7 @@ export function usePreviewServer({ addCfPreviewTokenHeader(headers, previewToken.value); headers["host"] = previewToken.host; const protocols = headers["sec-websocket-protocol"]; + logger.debug("protocols: ", protocols); const localWebsocket = new WebSocket( message, socket, From b3d91d9cb3e33545d5a182f74267f4b52644ae12 Mon Sep 17 00:00:00 2001 From: Cameron Robey Date: Tue, 27 Sep 2022 12:22:23 +0100 Subject: [PATCH 4/9] Use node http instead of faye-websocket --- packages/wrangler/src/proxy.ts | 91 ++++++++++++++++++++++------------ 1 file changed, 60 insertions(+), 31 deletions(-) diff --git a/packages/wrangler/src/proxy.ts b/packages/wrangler/src/proxy.ts index c5244b5aaae..bac6d625148 100644 --- a/packages/wrangler/src/proxy.ts +++ b/packages/wrangler/src/proxy.ts @@ -1,14 +1,15 @@ import { createServer as createHttpServer } from "node:http"; import { connect } from "node:http2"; import { createServer as createHttpsServer } from "node:https"; +import https from "node:https"; import { networkInterfaces } from "node:os"; -import WebSocket from "faye-websocket"; import { createHttpTerminator } from "http-terminator"; import { useEffect, useRef, useState } from "react"; import serveStatic from "serve-static"; import { getHttpsOptions } from "./https-options"; import { logger } from "./logger"; import type { CfPreviewToken } from "./create-worker-preview"; +import type WebSocket from "faye-websocket"; import type { HttpTerminator } from "http-terminator"; import type { IncomingHttpHeaders, @@ -19,12 +20,7 @@ import type { } from "node:http"; import type { ClientHttp2Session, ServerHttp2Stream } from "node:http2"; import type { Server as HttpsServer } from "node:https"; -import type ws from "ws"; - -interface IWebsocket extends ws { - // Pipe implements .on("message", ...) - pipe(fn: T): IWebsocket; -} +import type { Writable } from "node:stream"; /** * `usePreviewServer` is a React hook that creates a local development @@ -70,6 +66,26 @@ function rewriteRemoteHostToLocalHostInHeaders( } } +function writeHead( + socket: Writable, + res: Pick< + IncomingMessage, + "httpVersion" | "statusCode" | "statusMessage" | "headers" + > +) { + socket.write( + `HTTP/${res.httpVersion} ${res.statusCode} ${res.statusMessage}\r\n` + ); + for (const [key, values] of Object.entries(res.headers)) { + if (Array.isArray(values)) { + for (const value of values) socket.write(`${key}: ${value}\r\n`); + } else { + socket.write(`${key}: ${values}\r\n`); + } + } + socket.write("\r\n"); +} + type PreviewProxy = { server: HttpServer | HttpsServer; terminator: HttpTerminator; @@ -273,34 +289,47 @@ export function usePreviewServer({ /** HTTP/1 -> WebSocket (over HTTP/1) */ const handleUpgrade = ( - message: IncomingMessage, - socket: WebSocket, - body: Buffer + originalMessage: IncomingMessage, + originalSocket: WebSocket, + originalBody: Buffer ) => { - const { headers, url } = message; + const { headers, method, url } = originalMessage; addCfPreviewTokenHeader(headers, previewToken.value); headers["host"] = previewToken.host; - const protocols = headers["sec-websocket-protocol"]; - logger.debug("protocols: ", protocols); - const localWebsocket = new WebSocket( - message, - socket, - body, - protocols - ) as IWebsocket; - - const remoteWebsocketClient = new WebSocket.Client( - `wss://${previewToken.host}${url}`, - protocols, - { headers } - ) as IWebsocket; - localWebsocket.pipe(remoteWebsocketClient).pipe(localWebsocket); - // We close down websockets whenever we refresh the token. - cleanupListeners.push(() => { - localWebsocket.close(); - remoteWebsocketClient.close(); - }); + + if (originalBody?.byteLength) originalSocket.unshift(originalBody); + + const runtimeRequest = https.request( + { + hostname: `${previewToken.host}`, + path: url, + method, + headers, + }, + (runtimeResponse) => { + if (!(runtimeResponse as { upgrade?: boolean }).upgrade) { + writeHead(originalSocket, runtimeResponse); + runtimeResponse.pipe(originalSocket); + } + } + ); + + runtimeRequest.on( + "upgrade", + (runtimeResponse, runtimeSocket, runtimeHead) => { + if (runtimeHead?.byteLength) runtimeSocket.unshift(runtimeHead); + writeHead(originalSocket, { + httpVersion: "1.1", + statusCode: 101, + statusMessage: "Switching Protocols", + headers: runtimeResponse.headers, + }); + runtimeSocket.pipe(originalSocket).pipe(runtimeSocket); + } + ); + originalMessage.pipe(runtimeRequest); }; + proxy.server.on("upgrade", handleUpgrade); cleanupListeners.push(() => proxy.server.off("upgrade", handleUpgrade)); From a1c4bf978c11b23642665df7ab0c8589c9ffd489 Mon Sep 17 00:00:00 2001 From: Cameron Robey Date: Tue, 27 Sep 2022 12:56:02 +0100 Subject: [PATCH 5/9] Remove dependence on faye-websocket --- package-lock.json | 63 ----------------------- packages/wrangler/package.json | 1 - packages/wrangler/src/faye-websocket.d.ts | 6 --- packages/wrangler/src/proxy.ts | 5 +- 4 files changed, 2 insertions(+), 73 deletions(-) delete mode 100644 packages/wrangler/src/faye-websocket.d.ts diff --git a/package-lock.json b/package-lock.json index ac92be922bd..c55df1e58f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8412,17 +8412,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/fb-watchman": { "version": "2.0.1", "license": "Apache-2.0", @@ -9269,11 +9258,6 @@ "node": ">= 0.8" } }, - "node_modules/http-parser-js": { - "version": "0.5.5", - "dev": true, - "license": "MIT" - }, "node_modules/http-proxy-agent": { "version": "4.0.1", "dev": true, @@ -21352,27 +21336,6 @@ "node": ">=0.10.0" } }, - "node_modules/websocket-driver": { - "version": "0.7.4", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/whatwg-encoding": { "version": "1.0.5", "dev": true, @@ -21920,7 +21883,6 @@ "dotenv": "^16.0.0", "execa": "^6.1.0", "express": "^4.18.1", - "faye-websocket": "^0.11.4", "finalhandler": "^1.2.0", "find-up": "^6.3.0", "get-port": "^6.1.2", @@ -28774,13 +28736,6 @@ "format": "^0.2.0" } }, - "faye-websocket": { - "version": "0.11.4", - "dev": true, - "requires": { - "websocket-driver": ">=0.5.1" - } - }, "fb-watchman": { "version": "2.0.1", "requires": { @@ -29329,10 +29284,6 @@ "toidentifier": "1.0.1" } }, - "http-parser-js": { - "version": "0.5.5", - "dev": true - }, "http-proxy-agent": { "version": "4.0.1", "dev": true, @@ -37217,19 +37168,6 @@ } } }, - "websocket-driver": { - "version": "0.7.4", - "dev": true, - "requires": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - } - }, - "websocket-extensions": { - "version": "0.1.4", - "dev": true - }, "whatwg-encoding": { "version": "1.0.5", "dev": true, @@ -37370,7 +37308,6 @@ "esbuild": "0.14.51", "execa": "^6.1.0", "express": "^4.18.1", - "faye-websocket": "^0.11.4", "finalhandler": "^1.2.0", "find-up": "^6.3.0", "fsevents": "~2.3.2", diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index 52438b43da2..074a13a3d4a 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -139,7 +139,6 @@ "dotenv": "^16.0.0", "execa": "^6.1.0", "express": "^4.18.1", - "faye-websocket": "^0.11.4", "finalhandler": "^1.2.0", "find-up": "^6.3.0", "get-port": "^6.1.2", diff --git a/packages/wrangler/src/faye-websocket.d.ts b/packages/wrangler/src/faye-websocket.d.ts deleted file mode 100644 index d6e5e009152..00000000000 --- a/packages/wrangler/src/faye-websocket.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -module "faye-websocket" { - /** - * Standards-compliant WebSocket client and server. - */ - export default WebSocket; -} diff --git a/packages/wrangler/src/proxy.ts b/packages/wrangler/src/proxy.ts index bac6d625148..01de17ae42b 100644 --- a/packages/wrangler/src/proxy.ts +++ b/packages/wrangler/src/proxy.ts @@ -9,7 +9,6 @@ import serveStatic from "serve-static"; import { getHttpsOptions } from "./https-options"; import { logger } from "./logger"; import type { CfPreviewToken } from "./create-worker-preview"; -import type WebSocket from "faye-websocket"; import type { HttpTerminator } from "http-terminator"; import type { IncomingHttpHeaders, @@ -20,7 +19,7 @@ import type { } from "node:http"; import type { ClientHttp2Session, ServerHttp2Stream } from "node:http2"; import type { Server as HttpsServer } from "node:https"; -import type { Writable } from "node:stream"; +import type { Duplex, Writable } from "node:stream"; /** * `usePreviewServer` is a React hook that creates a local development @@ -290,7 +289,7 @@ export function usePreviewServer({ /** HTTP/1 -> WebSocket (over HTTP/1) */ const handleUpgrade = ( originalMessage: IncomingMessage, - originalSocket: WebSocket, + originalSocket: Duplex, originalBody: Buffer ) => { const { headers, method, url } = originalMessage; From 60dbecc5f0ad57bdccdf04eac260c8bcfed08266 Mon Sep 17 00:00:00 2001 From: Max Rozen <3822106+rozenmd@users.noreply.github.com> Date: Tue, 27 Sep 2022 17:07:07 +0200 Subject: [PATCH 6/9] Update changeset Update .changeset/tall-radios-report.md --- .changeset/tall-radios-report.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.changeset/tall-radios-report.md b/.changeset/tall-radios-report.md index b868d9e3a5c..f75a5fef43c 100644 --- a/.changeset/tall-radios-report.md +++ b/.changeset/tall-radios-report.md @@ -2,6 +2,12 @@ "wrangler": patch --- -fix: pass protocols from headers for wrangler dev +fix: use node http instead of faye-websocket in proxy server -Prior to this change, the protocol passed between the client and the worker was being stripped out by wrangler. +We change how websockets are handled in the proxy server, fixing multiple issues of websocket behaviour, particularly to do with headers. + +In particular this fixes: + +- the protocol passed between the client and the worker was being stripped out by wrangler +- wrangler was discarding additional headesr from websocket upgrade response +- websocket close code and reason was not being propagated by wrangler From f6fc7a3ea9d3b70952ca7ff11900f9943d604719 Mon Sep 17 00:00:00 2001 From: Max Rozen <3822106+rozenmd@users.noreply.github.com> Date: Wed, 28 Sep 2022 09:58:22 +0200 Subject: [PATCH 7/9] Update packages/wrangler/src/proxy.ts Co-authored-by: MrBBot --- packages/wrangler/src/proxy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wrangler/src/proxy.ts b/packages/wrangler/src/proxy.ts index 01de17ae42b..e567185add8 100644 --- a/packages/wrangler/src/proxy.ts +++ b/packages/wrangler/src/proxy.ts @@ -290,7 +290,7 @@ export function usePreviewServer({ const handleUpgrade = ( originalMessage: IncomingMessage, originalSocket: Duplex, - originalBody: Buffer + originalHead: Buffer ) => { const { headers, method, url } = originalMessage; addCfPreviewTokenHeader(headers, previewToken.value); From cf887649a82b862364469910d6b9699500dacc31 Mon Sep 17 00:00:00 2001 From: Max Rozen <3822106+rozenmd@users.noreply.github.com> Date: Wed, 28 Sep 2022 10:05:31 +0200 Subject: [PATCH 8/9] Update packages/wrangler/src/proxy.ts --- packages/wrangler/src/proxy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wrangler/src/proxy.ts b/packages/wrangler/src/proxy.ts index e567185add8..9c5a4a3e22e 100644 --- a/packages/wrangler/src/proxy.ts +++ b/packages/wrangler/src/proxy.ts @@ -296,7 +296,7 @@ export function usePreviewServer({ addCfPreviewTokenHeader(headers, previewToken.value); headers["host"] = previewToken.host; - if (originalBody?.byteLength) originalSocket.unshift(originalBody); + if (originalHead?.byteLength) originalSocket.unshift(originalHead); const runtimeRequest = https.request( { From 6fe426f058f92c3e89a59f83852fc47a632fe20b Mon Sep 17 00:00:00 2001 From: Cameron Robey Date: Wed, 28 Sep 2022 09:44:04 +0100 Subject: [PATCH 9/9] Remove string coercion --- packages/wrangler/src/proxy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wrangler/src/proxy.ts b/packages/wrangler/src/proxy.ts index 9c5a4a3e22e..49d41b2b1f2 100644 --- a/packages/wrangler/src/proxy.ts +++ b/packages/wrangler/src/proxy.ts @@ -300,7 +300,7 @@ export function usePreviewServer({ const runtimeRequest = https.request( { - hostname: `${previewToken.host}`, + hostname: previewToken.host, path: url, method, headers,