mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-14 04:32:03 +00:00
fix(multiplayer): harden websocket frame parser
This commit is contained in:
@@ -1459,6 +1459,8 @@ The matchmaking UI now exposes that retained replay through its existing action
|
|||||||
|
|
||||||
The control plane now exports bounded Prometheus-compatible API request counters and latency summaries at `GET /metrics`, with fixed operation/status labels and no event-stream wrapping. Production and testkit services wire the collector; adversarial tests verify unknown paths cannot inject label cardinality or leak URL secrets, and full Go/race/vet checks pass. Durable SLO dashboards and alert routing remain operational work.
|
The control plane now exports bounded Prometheus-compatible API request counters and latency summaries at `GET /metrics`, with fixed operation/status labels and no event-stream wrapping. Production and testkit services wire the collector; adversarial tests verify unknown paths cannot inject label cardinality or leak URL secrets, and full Go/race/vet checks pass. Durable SLO dashboards and alert routing remain operational work.
|
||||||
|
|
||||||
|
The authenticated event stream now rejects client data/reserved opcodes and oversized control frames at the parser boundary; only RFC 6455 close, ping, and pong frames are accepted from clients, preserving the bounded v1 stream contract.
|
||||||
|
|
||||||
`make verify-multiplayer-local` now provides one cloud-free regression gate for the current implementation: the complete Go suite, the Godot harness, OpenAPI parsing, and the migration/Fleet/Kubernetes/supply-chain checks. It fails clearly when the configured Godot executable is unavailable and does not weaken or replace the existing Phase 6/ENet gates; PostgreSQL, Redis, Steam, Agones, and multi-process Internet gates remain separate.
|
`make verify-multiplayer-local` now provides one cloud-free regression gate for the current implementation: the complete Go suite, the Godot harness, OpenAPI parsing, and the migration/Fleet/Kubernetes/supply-chain checks. It fails clearly when the configured Godot executable is unavailable and does not weaken or replace the existing Phase 6/ENet gates; PostgreSQL, Redis, Steam, Agones, and multi-process Internet gates remain separate.
|
||||||
|
|
||||||
That local gate now also runs `go test -race ./...`, `go vet ./...`, and each declared domain fuzz target for a bounded 2-second interval, aligning the one-command gate with the separately recorded 8.46 verification requirements.
|
That local gate now also runs `go test -race ./...`, `go vet ./...`, and each declared domain fuzz target for a bounded 2-second interval, aligning the one-command gate with the separately recorded 8.46 verification requirements.
|
||||||
|
|||||||
@@ -317,6 +317,10 @@ func readWebSocketFrame(reader *bufio.Reader) (byte, []byte, error) {
|
|||||||
if first&0x70 != 0 || first&0x80 == 0 {
|
if first&0x70 != 0 || first&0x80 == 0 {
|
||||||
return 0, nil, errors.New("unsupported websocket frame")
|
return 0, nil, errors.New("unsupported websocket frame")
|
||||||
}
|
}
|
||||||
|
opcode := first & 0x0f
|
||||||
|
if opcode != 0x8 && opcode != 0x9 && opcode != 0xA {
|
||||||
|
return 0, nil, errors.New("unsupported websocket opcode")
|
||||||
|
}
|
||||||
if second&0x80 == 0 {
|
if second&0x80 == 0 {
|
||||||
return 0, nil, errors.New("unmasked websocket frame")
|
return 0, nil, errors.New("unmasked websocket frame")
|
||||||
}
|
}
|
||||||
@@ -337,6 +341,9 @@ func readWebSocketFrame(reader *bufio.Reader) (byte, []byte, error) {
|
|||||||
if length > maxWebSocketFrame {
|
if length > maxWebSocketFrame {
|
||||||
return 0, nil, errors.New("websocket frame too large")
|
return 0, nil, errors.New("websocket frame too large")
|
||||||
}
|
}
|
||||||
|
if opcode&0x8 != 0 && length > 125 {
|
||||||
|
return 0, nil, errors.New("websocket control frame too large")
|
||||||
|
}
|
||||||
var mask [4]byte
|
var mask [4]byte
|
||||||
if _, err := io.ReadFull(reader, mask[:]); err != nil {
|
if _, err := io.ReadFull(reader, mask[:]); err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
@@ -348,7 +355,7 @@ func readWebSocketFrame(reader *bufio.Reader) (byte, []byte, error) {
|
|||||||
for i := range payload {
|
for i := range payload {
|
||||||
payload[i] ^= mask[i%4]
|
payload[i] ^= mask[i%4]
|
||||||
}
|
}
|
||||||
return first & 0x0f, payload, nil
|
return opcode, payload, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeWebSocketFrame(connection net.Conn, opcode byte, payload []byte) error {
|
func writeWebSocketFrame(connection net.Conn, opcode byte, payload []byte) error {
|
||||||
|
|||||||
@@ -1,12 +1,30 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestWebSocketReaderRejectsClientDataAndReservedOpcodes(t *testing.T) {
|
||||||
|
for _, opcode := range []byte{0x0, 0x1, 0x2, 0x3, 0xB, 0xF} {
|
||||||
|
frame := append([]byte{0x80 | opcode, 0x80, 0, 0, 0, 0}, nil...)
|
||||||
|
if _, _, err := readWebSocketFrame(bufio.NewReader(bytes.NewReader(frame))); err == nil {
|
||||||
|
t.Fatalf("opcode 0x%x was accepted", opcode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebSocketReaderRejectsOversizedControlFrame(t *testing.T) {
|
||||||
|
frame := append([]byte{0x89, 0xFE, 0, 126, 0, 0, 0, 0}, bytes.Repeat([]byte{0}, 126)...)
|
||||||
|
if _, _, err := readWebSocketFrame(bufio.NewReader(bytes.NewReader(frame))); err == nil {
|
||||||
|
t.Fatal("oversized ping control frame was accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestWebSocketHandshakeRequiresRFC6455Version(t *testing.T) {
|
func TestWebSocketHandshakeRequiresRFC6455Version(t *testing.T) {
|
||||||
service := &Service{}
|
service := &Service{}
|
||||||
request := httptest.NewRequest(http.MethodGet, "/v1/events", nil)
|
request := httptest.NewRequest(http.MethodGet, "/v1/events", nil)
|
||||||
|
|||||||
Reference in New Issue
Block a user