Files
devplacepy/nodes/client/ws.go
T
retoor 6f80a820da
DevPlace CI / test (push) Failing after 3m13s
Zo doen wijdat in oost
2026-10-16 07:20:44 +02:00

359 lines
8.7 KiB
Go

package main
import (
"bufio"
"context"
"crypto/rand"
"crypto/sha1"
"crypto/tls"
"encoding/base64"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strings"
"sync"
"time"
"unicode/utf8"
)
const (
opContinuation = 0x0
opText = 0x1
opBinary = 0x2
opClose = 0x8
opPing = 0x9
opPong = 0xA
wsGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
wsWriteTimeout = 30 * time.Second
wsHandshakeLimit = 15 * time.Second
maxControlBytes = 125
)
type CloseError struct {
Code int
Reason string
}
func (e *CloseError) Error() string {
return fmt.Sprintf("websocket closed code=%d reason=%q", e.Code, e.Reason)
}
type Conn struct {
rwc net.Conn
br *bufio.Reader
client bool
maxMessage int
wmu sync.Mutex
closed bool
}
func newConn(rwc net.Conn, br *bufio.Reader, client bool) *Conn {
return &Conn{rwc: rwc, br: br, client: client, maxMessage: MaxInboundMessage}
}
func Dial(ctx context.Context, rawURL string) (*Conn, error) {
target, err := url.Parse(rawURL)
if err != nil {
return nil, errors.New("websocket url cannot be parsed")
}
secure := false
switch target.Scheme {
case "wss", "https":
secure = true
case "ws", "http":
default:
return nil, errors.New("websocket url scheme must be ws, wss, http or https")
}
host := target.Hostname()
port := target.Port()
if port == "" {
port = "80"
if secure {
port = "443"
}
}
address := net.JoinHostPort(host, port)
dialer := &net.Dialer{Timeout: wsHandshakeLimit}
raw, err := dialer.DialContext(ctx, "tcp", address)
if err != nil {
return nil, fmt.Errorf("websocket connect to %s failed: %w", address, stripURL(err))
}
var rwc net.Conn = raw
if secure {
tlsConn := tls.Client(raw, &tls.Config{ServerName: host, MinVersion: tls.VersionTLS12})
_ = raw.SetDeadline(time.Now().Add(wsHandshakeLimit))
if err := tlsConn.HandshakeContext(ctx); err != nil {
raw.Close()
return nil, fmt.Errorf("websocket tls handshake failed: %w", err)
}
rwc = tlsConn
}
_ = rwc.SetDeadline(time.Now().Add(wsHandshakeLimit))
keyBytes := make([]byte, 16)
if _, err := rand.Read(keyBytes); err != nil {
rwc.Close()
return nil, err
}
key := base64.StdEncoding.EncodeToString(keyBytes)
path := target.RequestURI()
if path == "" {
path = "/"
}
request := fmt.Sprintf(
"GET %s HTTP/1.1\r\nHost: %s\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: %s\r\nSec-WebSocket-Version: 13\r\nUser-Agent: devplace-node/%s\r\n\r\n",
path, target.Host, key, Version,
)
if _, err := io.WriteString(rwc, request); err != nil {
rwc.Close()
return nil, fmt.Errorf("websocket handshake write failed: %w", err)
}
br := bufio.NewReaderSize(rwc, 16*1024)
response, err := http.ReadResponse(br, &http.Request{Method: "GET"})
if err != nil {
rwc.Close()
return nil, fmt.Errorf("websocket handshake read failed: %w", err)
}
if response.StatusCode != http.StatusSwitchingProtocols {
rwc.Close()
return nil, fmt.Errorf("websocket handshake refused with http status %d", response.StatusCode)
}
if !strings.EqualFold(response.Header.Get("Upgrade"), "websocket") ||
response.Header.Get("Sec-WebSocket-Accept") != acceptKey(key) {
rwc.Close()
return nil, errors.New("websocket handshake answer is invalid")
}
_ = rwc.SetDeadline(time.Time{})
return newConn(rwc, br, true), nil
}
func stripURL(err error) error {
var urlErr *url.Error
if errors.As(err, &urlErr) {
return urlErr.Err
}
return err
}
func acceptKey(key string) string {
sum := sha1.Sum([]byte(key + wsGUID))
return base64.StdEncoding.EncodeToString(sum[:])
}
func (c *Conn) SetReadDeadline(t time.Time) error { return c.rwc.SetReadDeadline(t) }
func (c *Conn) Close() error {
c.wmu.Lock()
c.closed = true
c.wmu.Unlock()
return c.rwc.Close()
}
func (c *Conn) writeFrame(fin bool, op byte, payload []byte) error {
header := make([]byte, 0, 14)
first := op
if fin {
first |= 0x80
}
header = append(header, first)
maskBit := byte(0)
if c.client {
maskBit = 0x80
}
size := len(payload)
switch {
case size <= 125:
header = append(header, maskBit|byte(size))
case size <= 0xFFFF:
header = append(header, maskBit|126, 0, 0)
binary.BigEndian.PutUint16(header[len(header)-2:], uint16(size))
default:
header = append(header, maskBit|127, 0, 0, 0, 0, 0, 0, 0, 0)
binary.BigEndian.PutUint64(header[len(header)-8:], uint64(size))
}
body := payload
if c.client {
mask := make([]byte, 4)
if _, err := rand.Read(mask); err != nil {
return err
}
header = append(header, mask...)
body = make([]byte, size)
for i := range payload {
body[i] = payload[i] ^ mask[i%4]
}
}
if err := c.rwc.SetWriteDeadline(time.Now().Add(wsWriteTimeout)); err != nil {
return err
}
if _, err := c.rwc.Write(header); err != nil {
return err
}
_, err := c.rwc.Write(body)
return err
}
func (c *Conn) WriteText(text string) error {
data := []byte(text)
c.wmu.Lock()
defer c.wmu.Unlock()
if c.closed {
return net.ErrClosed
}
op := byte(opText)
for first := true; first || len(data) > 0; first = false {
chunk := data
if len(chunk) > MaxWSFramePayload {
chunk = data[:MaxWSFramePayload]
}
data = data[len(chunk):]
if err := c.writeFrame(len(data) == 0, op, chunk); err != nil {
return err
}
op = opContinuation
}
return nil
}
func (c *Conn) writeControl(op byte, payload []byte) error {
if len(payload) > maxControlBytes {
payload = payload[:maxControlBytes]
}
c.wmu.Lock()
defer c.wmu.Unlock()
if c.closed {
return net.ErrClosed
}
return c.writeFrame(true, op, payload)
}
func (c *Conn) WriteClose(code int, reason string) error {
if len(reason) > 100 {
reason = reason[:100]
}
payload := make([]byte, 2, 2+len(reason))
binary.BigEndian.PutUint16(payload, uint16(code))
payload = append(payload, reason...)
return c.writeControl(opClose, payload)
}
func (c *Conn) readFrame() (fin bool, op byte, payload []byte, err error) {
head := make([]byte, 2)
if _, err = io.ReadFull(c.br, head); err != nil {
return
}
fin = head[0]&0x80 != 0
if head[0]&0x70 != 0 {
err = &CloseError{Code: CloseProtocolError, Reason: "reserved bits set"}
return
}
op = head[0] & 0x0F
masked := head[1]&0x80 != 0
if masked == c.client {
err = &CloseError{Code: CloseProtocolError, Reason: "bad masking"}
return
}
length := uint64(head[1] & 0x7F)
switch length {
case 126:
ext := make([]byte, 2)
if _, err = io.ReadFull(c.br, ext); err != nil {
return
}
length = uint64(binary.BigEndian.Uint16(ext))
case 127:
ext := make([]byte, 8)
if _, err = io.ReadFull(c.br, ext); err != nil {
return
}
length = binary.BigEndian.Uint64(ext)
}
if op&0x8 != 0 && (length > maxControlBytes || !fin) {
err = &CloseError{Code: CloseProtocolError, Reason: "bad control frame"}
return
}
if length > uint64(c.maxMessage) || length > uint64(MaxResultMessage) {
err = &CloseError{Code: CloseTooLarge, Reason: "frame too large"}
return
}
var mask []byte
if masked {
mask = make([]byte, 4)
if _, err = io.ReadFull(c.br, mask); err != nil {
return
}
}
payload = make([]byte, length)
if _, err = io.ReadFull(c.br, payload); err != nil {
return
}
if masked {
for i := range payload {
payload[i] ^= mask[i%4]
}
}
return
}
func (c *Conn) fail(code int, reason string) error {
_ = c.WriteClose(code, reason)
return &CloseError{Code: code, Reason: reason}
}
func (c *Conn) ReadMessage() ([]byte, error) {
var message []byte
inProgress := false
for {
fin, op, payload, err := c.readFrame()
if err != nil {
var closeErr *CloseError
if errors.As(err, &closeErr) {
return nil, c.fail(closeErr.Code, closeErr.Reason)
}
return nil, err
}
switch op {
case opPing:
if err := c.writeControl(opPong, payload); err != nil {
return nil, err
}
case opPong:
case opClose:
code, reason := CloseNormal, ""
if len(payload) >= 2 {
code = int(binary.BigEndian.Uint16(payload))
reason = string(payload[2:])
}
_ = c.WriteClose(code, "")
return nil, &CloseError{Code: code, Reason: reason}
case opText, opBinary, opContinuation:
if op == opContinuation && !inProgress {
return nil, c.fail(CloseProtocolError, "unexpected continuation")
}
if op != opContinuation && inProgress {
return nil, c.fail(CloseProtocolError, "interleaved data frames")
}
if op == opBinary {
return nil, c.fail(CloseUnsupported, "binary frames are not supported")
}
inProgress = !fin
if len(message)+len(payload) > c.maxMessage {
return nil, c.fail(CloseTooLarge, "message too large")
}
message = append(message, payload...)
if fin {
if !utf8.Valid(message) {
return nil, c.fail(CloseInvalidPayload, "invalid utf-8")
}
return message, nil
}
default:
return nil, c.fail(CloseProtocolError, "unknown opcode")
}
}
}