forked from retoor/devplacepy
Zo doen wijdat in oost
This commit is contained in:
@@ -0,0 +1,358 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user