From edfc9ac1e1fb69311cfb2232ebe1ac1874383792 Mon Sep 17 00:00:00 2001 From: John Niang Date: Thu, 24 Nov 2022 19:31:06 +0800 Subject: [PATCH] Ignore websocket protocol while proxying console (#2760) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind feature /area core #### What this PR does / why we need it: Ignore websocket protocol while proxying console. If we don't do this, console dev environment will crash with following error: ```bash VITE v3.2.4 ready in 1672 ms ➜ Local: http://localhost:3000/console/ 16:12:21 ➜ Network: http://172.23.176.1:3000/console/ 16:12:21 ➜ Network: http://172.18.96.1:3000/console/ 16:12:21 ➜ Network: http://192.168.31.106:3000/console/ 16:12:21 [vite-plugin-static-copy] Collected 8 items. 16:12:21 node:events:491 throw er; // Unhandled 'error' event ^ RangeError: Invalid WebSocket frame: RSV1 must be clear at Receiver$1.getInfo (file:///C:/Users/johnn/workspaces/halo-dev/console/node_modules/.pnpm/vite@3.2.4_ajklay5k626t46b6fyghkbup3i/node_modules/vite/dist/node/chunks/dep-67e7f8ab.js:54186:14) at Receiver$1.startLoop (file:///C:/Users/johnn/workspaces/halo-dev/console/node_modules/.pnpm/vite@3.2.4_ajklay5k626t46b6fyghkbup3i/node_modules/vite/dist/node/chunks/dep-67e7f8ab.js:54133:22) at Receiver$1._write (file:///C:/Users/johnn/workspaces/halo-dev/console/node_modules/.pnpm/vite@3.2.4_ajklay5k626t46b6fyghkbup3i/node_modules/vite/dist/node/chunks/dep-67e7f8ab.js:54080:10) at writeOrBuffer (node:internal/streams/writable:392:12) at _write (node:internal/streams/writable:333:10) at Writable.write (node:internal/streams/writable:337:10) at Socket.socketOnData (file:///C:/Users/johnn/workspaces/halo-dev/console/node_modules/.pnpm/vite@3.2.4_ajklay5k626t46b6fyghkbup3i/node_modules/vite/dist/node/chunks/dep-67e7f8ab.js:56826:37) at Socket.emit (node:events:513:28) at addChunk (node:internal/streams/readable:324:12) at readableAddChunk (node:internal/streams/readable:297:9) at Readable.push (node:internal/streams/readable:234:10) at TCP.onStreamRead (node:internal/stream_base_commons:190:23) Emitted 'error' event on WebSocket$1 instance at: at Receiver$1.receiverOnError (file:///C:/Users/johnn/workspaces/halo-dev/console/node_modules/.pnpm/vite@3.2.4_ajklay5k626t46b6fyghkbup3i/node_modules/vite/dist/node/chunks/dep-67e7f8ab.js:56712:13) at Receiver$1.emit (node:events:513:28) at emitErrorNT (node:internal/streams/destroy:151:8) at emitErrorCloseNT (node:internal/streams/destroy:116:3) at process.processTicksAndRejections (node:internal/process/task_queues:82:21) { code: 'WS_ERR_UNEXPECTED_RSV_1', [Symbol(status-code)]: 1002 } Node.js v18.12.1  ELIFECYCLE  Command failed with exit code 1. ``` #### Special notes for your reviewer: Steps to test: 1. Edit your application.yaml with console proxy configuration 2. Start Halo 3. Use non-Chromelike browser to request . 4. See the result #### Does this PR introduce a user-facing change? ```release-note None ``` --- .../halo/app/console/ConsoleProxyFilter.java | 3 ++ .../WebSocketServerWebExchangeMatcher.java | 20 ++++++++++ ...WebSocketServerWebExchangeMatcherTest.java | 39 +++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 src/main/java/run/halo/app/console/WebSocketServerWebExchangeMatcher.java create mode 100644 src/test/java/run/halo/app/console/WebSocketServerWebExchangeMatcherTest.java diff --git a/src/main/java/run/halo/app/console/ConsoleProxyFilter.java b/src/main/java/run/halo/app/console/ConsoleProxyFilter.java index 8a0cba22d..3b927389d 100644 --- a/src/main/java/run/halo/app/console/ConsoleProxyFilter.java +++ b/src/main/java/run/halo/app/console/ConsoleProxyFilter.java @@ -5,6 +5,7 @@ import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; import org.springframework.web.reactive.function.BodyExtractors; @@ -31,6 +32,8 @@ public class ConsoleProxyFilter implements WebFilter { var consoleMatcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/console/**"); consoleMatcher = new AndServerWebExchangeMatcher(consoleMatcher, new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML)); + consoleMatcher = new AndServerWebExchangeMatcher(consoleMatcher, + new NegatedServerWebExchangeMatcher(new WebSocketServerWebExchangeMatcher())); this.consoleMatcher = consoleMatcher; this.webClient = WebClient.create(proxyProperties.getEndpoint().toString()); log.info("Initialized ConsoleProxyFilter to proxy console"); diff --git a/src/main/java/run/halo/app/console/WebSocketServerWebExchangeMatcher.java b/src/main/java/run/halo/app/console/WebSocketServerWebExchangeMatcher.java new file mode 100644 index 000000000..7b8cb440e --- /dev/null +++ b/src/main/java/run/halo/app/console/WebSocketServerWebExchangeMatcher.java @@ -0,0 +1,20 @@ +package run.halo.app.console; + +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +public class WebSocketServerWebExchangeMatcher implements ServerWebExchangeMatcher { + @Override + public Mono matches(ServerWebExchange exchange) { + var headers = exchange.getRequest().getHeaders(); + if (!headers.getConnection().contains("Upgrade")) { + return MatchResult.notMatch(); + } + var upgrade = headers.getUpgrade(); + if (!"websocket".equalsIgnoreCase(upgrade)) { + return MatchResult.notMatch(); + } + return MatchResult.match(); + } +} diff --git a/src/test/java/run/halo/app/console/WebSocketServerWebExchangeMatcherTest.java b/src/test/java/run/halo/app/console/WebSocketServerWebExchangeMatcherTest.java new file mode 100644 index 000000000..cf2944bc2 --- /dev/null +++ b/src/test/java/run/halo/app/console/WebSocketServerWebExchangeMatcherTest.java @@ -0,0 +1,39 @@ +package run.halo.app.console; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import reactor.test.StepVerifier; + +class WebSocketServerWebExchangeMatcherTest { + + @Test + void shouldMatchIfWebSocketProtocol() { + var httpRequest = MockServerHttpRequest.get("") + .header(HttpHeaders.CONNECTION, HttpHeaders.UPGRADE) + .header(HttpHeaders.UPGRADE, "websocket") + .build(); + var wsExchange = MockServerWebExchange.from(httpRequest); + var wsMatcher = new WebSocketServerWebExchangeMatcher(); + StepVerifier.create(wsMatcher.matches(wsExchange)) + .consumeNextWith(result -> assertTrue(result.isMatch())) + .verifyComplete(); + } + + @Test + void shouldNotMatchIfNotWebSocketProtocol() { + var httpRequest = MockServerHttpRequest.get("") + .header(HttpHeaders.CONNECTION, HttpHeaders.UPGRADE) + .header(HttpHeaders.UPGRADE, "not-a-websocket") + .build(); + var wsExchange = MockServerWebExchange.from(httpRequest); + var wsMatcher = new WebSocketServerWebExchangeMatcher(); + StepVerifier.create(wsMatcher.matches(wsExchange)) + .consumeNextWith(result -> assertFalse(result.isMatch())) + .verifyComplete(); + } +} \ No newline at end of file