221 lines
5.8 KiB
Go
221 lines
5.8 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func upgrade(w http.ResponseWriter, r *http.Request) (*Conn, error) {
|
|
hijacker := w.(http.Hijacker)
|
|
rwc, brw, err := hijacker.Hijack()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
answer := "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: " +
|
|
acceptKey(r.Header.Get("Sec-WebSocket-Key")) + "\r\n\r\n"
|
|
if _, err := rwc.Write([]byte(answer)); err != nil {
|
|
return nil, err
|
|
}
|
|
conn := newConn(rwc, brw.Reader, false)
|
|
conn.maxMessage = MaxResultMessage
|
|
return conn, nil
|
|
}
|
|
|
|
func wsServer(t *testing.T, handler func(*Conn)) *httptest.Server {
|
|
t.Helper()
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
conn, err := upgrade(w, r)
|
|
if err != nil {
|
|
t.Errorf("upgrade: %v", err)
|
|
return
|
|
}
|
|
defer conn.Close()
|
|
handler(conn)
|
|
}))
|
|
t.Cleanup(server.Close)
|
|
return server
|
|
}
|
|
|
|
func dialTest(t *testing.T, server *httptest.Server) *Conn {
|
|
t.Helper()
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
conn, err := Dial(ctx, "ws"+strings.TrimPrefix(server.URL, "http")+"/socket")
|
|
if err != nil {
|
|
t.Fatalf("dial: %v", err)
|
|
}
|
|
t.Cleanup(func() { conn.Close() })
|
|
_ = conn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
|
return conn
|
|
}
|
|
|
|
func TestAcceptKeyVector(t *testing.T) {
|
|
if got := acceptKey("dGhlIHNhbXBsZSBub25jZQ=="); got != "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=" {
|
|
t.Fatalf("accept key %s", got)
|
|
}
|
|
}
|
|
|
|
func TestRoundTripAndFragmentation(t *testing.T) {
|
|
sizes := []int{0, 1, 125, 126, 65535, 65536, MaxWSFramePayload, MaxWSFramePayload + 1, 700 * 1024}
|
|
server := wsServer(t, func(c *Conn) {
|
|
for range sizes {
|
|
message, err := c.ReadMessage()
|
|
if err != nil {
|
|
t.Errorf("server read: %v", err)
|
|
return
|
|
}
|
|
if len(message) <= MaxInboundMessage {
|
|
if err := c.WriteText(string(message)); err != nil {
|
|
t.Errorf("server write: %v", err)
|
|
return
|
|
}
|
|
} else if err := c.WriteText("big"); err != nil {
|
|
return
|
|
}
|
|
}
|
|
})
|
|
conn := dialTest(t, server)
|
|
for _, size := range sizes {
|
|
text := strings.Repeat("a", size)
|
|
if err := conn.WriteText(text); err != nil {
|
|
t.Fatalf("write %d: %v", size, err)
|
|
}
|
|
got, err := conn.ReadMessage()
|
|
if err != nil {
|
|
t.Fatalf("read %d: %v", size, err)
|
|
}
|
|
want := text
|
|
if size > MaxInboundMessage {
|
|
want = "big"
|
|
}
|
|
if string(got) != want {
|
|
t.Fatalf("size %d: got %d bytes", size, len(got))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPingAnsweredWithPong(t *testing.T) {
|
|
done := make(chan []byte, 1)
|
|
server := wsServer(t, func(c *Conn) {
|
|
_ = c.writeControl(opPing, []byte("probe"))
|
|
_ = c.WriteText("after")
|
|
_, op, payload, err := c.readFrame()
|
|
if err != nil || op != opPong {
|
|
t.Errorf("expected pong, got op %d err %v", op, err)
|
|
}
|
|
done <- payload
|
|
})
|
|
conn := dialTest(t, server)
|
|
message, err := conn.ReadMessage()
|
|
if err != nil || string(message) != "after" {
|
|
t.Fatalf("message %q err %v", message, err)
|
|
}
|
|
if got := <-done; !bytes.Equal(got, []byte("probe")) {
|
|
t.Fatalf("pong payload %q", got)
|
|
}
|
|
}
|
|
|
|
func TestClientFramesAreMasked(t *testing.T) {
|
|
masked := make(chan bool, 1)
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
conn, err := upgrade(w, r)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer conn.Close()
|
|
head := make([]byte, 2)
|
|
if _, err := conn.br.Read(head); err != nil {
|
|
return
|
|
}
|
|
masked <- head[1]&0x80 != 0
|
|
}))
|
|
defer server.Close()
|
|
conn := dialTest(t, server)
|
|
if err := conn.WriteText("x"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !<-masked {
|
|
t.Fatal("client frame was not masked")
|
|
}
|
|
}
|
|
|
|
func TestCloseCodeSurfaces(t *testing.T) {
|
|
server := wsServer(t, func(c *Conn) { _ = c.WriteClose(CloseRetryOwner, "owner") })
|
|
conn := dialTest(t, server)
|
|
_, err := conn.ReadMessage()
|
|
var closeErr *CloseError
|
|
if !errors.As(err, &closeErr) || closeErr.Code != CloseRetryOwner || closeErr.Reason != "owner" {
|
|
t.Fatalf("got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestOversizeFrameRejectedWithClose(t *testing.T) {
|
|
server := wsServer(t, func(c *Conn) {
|
|
_ = c.writeFrame(true, opText, []byte(strings.Repeat("b", MaxWSFramePayload+1)))
|
|
_, _, _, _ = c.readFrame()
|
|
})
|
|
conn := dialTest(t, server)
|
|
_, err := conn.ReadMessage()
|
|
var closeErr *CloseError
|
|
if !errors.As(err, &closeErr) || closeErr.Code != CloseTooLarge {
|
|
t.Fatalf("got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestOversizeMessageAcrossFragmentsRejected(t *testing.T) {
|
|
server := wsServer(t, func(c *Conn) {
|
|
chunk := []byte(strings.Repeat("c", MaxWSFramePayload))
|
|
_ = c.writeFrame(false, opText, chunk)
|
|
_ = c.writeFrame(true, opContinuation, chunk)
|
|
_, _, _, _ = c.readFrame()
|
|
})
|
|
conn := dialTest(t, server)
|
|
_, err := conn.ReadMessage()
|
|
var closeErr *CloseError
|
|
if !errors.As(err, &closeErr) || closeErr.Code != CloseTooLarge {
|
|
t.Fatalf("got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBinaryAndBadUTF8Rejected(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
op byte
|
|
payload []byte
|
|
code int
|
|
}{
|
|
{"binary", opBinary, []byte("x"), CloseUnsupported},
|
|
{"utf8", opText, []byte{0xff, 0xfe}, CloseInvalidPayload},
|
|
{"continuation", opContinuation, []byte("x"), CloseProtocolError},
|
|
}
|
|
for _, item := range cases {
|
|
server := wsServer(t, func(c *Conn) {
|
|
_ = c.writeFrame(true, item.op, item.payload)
|
|
_, _, _, _ = c.readFrame()
|
|
})
|
|
conn := dialTest(t, server)
|
|
_, err := conn.ReadMessage()
|
|
var closeErr *CloseError
|
|
if !errors.As(err, &closeErr) || closeErr.Code != item.code {
|
|
t.Fatalf("%s: got %v", item.name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHandshakeRefusal(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
http.Error(w, "no", http.StatusForbidden)
|
|
}))
|
|
defer server.Close()
|
|
_, err := Dial(context.Background(), "ws"+strings.TrimPrefix(server.URL, "http"))
|
|
if err == nil || !strings.Contains(err.Error(), "403") {
|
|
t.Fatalf("got %v", err)
|
|
}
|
|
}
|