Ignore websocket protocol while proxying console (#2760)

#### 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 <http://localhost:8090>.
4. See the result

#### Does this PR introduce a user-facing change?

```release-note
None
```
pull/2741/head^2
John Niang 2022-11-24 19:31:06 +08:00 committed by GitHub
parent f96ef7f1b3
commit edfc9ac1e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 62 additions and 0 deletions

View File

@ -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");

View File

@ -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<MatchResult> 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();
}
}

View File

@ -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();
}
}