359 lines
8.7 KiB
Go
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")
|
|
}
|
|
}
|
|
}
|