421 lines
13 KiB
Go
421 lines
13 KiB
Go
package ws
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"math/rand"
|
|
)
|
|
|
|
// Constants defined by specification.
|
|
const (
|
|
// All control frames MUST have a payload length of 125 bytes or less and MUST NOT be fragmented.
|
|
MaxControlFramePayloadSize = 125
|
|
)
|
|
|
|
// OpCode represents operation code.
|
|
type OpCode byte
|
|
|
|
// Operation codes defined by specification.
|
|
// See https://tools.ietf.org/html/rfc6455#section-5.2
|
|
const (
|
|
OpContinuation OpCode = 0x0
|
|
OpText OpCode = 0x1
|
|
OpBinary OpCode = 0x2
|
|
OpClose OpCode = 0x8
|
|
OpPing OpCode = 0x9
|
|
OpPong OpCode = 0xa
|
|
)
|
|
|
|
// IsControl checks whether the c is control operation code.
|
|
// See https://tools.ietf.org/html/rfc6455#section-5.5
|
|
func (c OpCode) IsControl() bool {
|
|
// RFC6455: Control frames are identified by opcodes where
|
|
// the most significant bit of the opcode is 1.
|
|
//
|
|
// Note that OpCode is only 4 bit length.
|
|
return c&0x8 != 0
|
|
}
|
|
|
|
// IsData checks whether the c is data operation code.
|
|
// See https://tools.ietf.org/html/rfc6455#section-5.6
|
|
func (c OpCode) IsData() bool {
|
|
// RFC6455: Data frames (e.g., non-control frames) are identified by opcodes
|
|
// where the most significant bit of the opcode is 0.
|
|
//
|
|
// Note that OpCode is only 4 bit length.
|
|
return c&0x8 == 0
|
|
}
|
|
|
|
// IsReserved checks whether the c is reserved operation code.
|
|
// See https://tools.ietf.org/html/rfc6455#section-5.2
|
|
func (c OpCode) IsReserved() bool {
|
|
// RFC6455:
|
|
// %x3-7 are reserved for further non-control frames
|
|
// %xB-F are reserved for further control frames
|
|
return (0x3 <= c && c <= 0x7) || (0xb <= c && c <= 0xf)
|
|
}
|
|
|
|
// StatusCode represents the encoded reason for closure of websocket connection.
|
|
//
|
|
// There are few helper methods on StatusCode that helps to define a range in
|
|
// which given code is lay in. accordingly to ranges defined in specification.
|
|
//
|
|
// See https://tools.ietf.org/html/rfc6455#section-7.4
|
|
type StatusCode uint16
|
|
|
|
// StatusCodeRange describes range of StatusCode values.
|
|
type StatusCodeRange struct {
|
|
Min, Max StatusCode
|
|
}
|
|
|
|
// Status code ranges defined by specification.
|
|
// See https://tools.ietf.org/html/rfc6455#section-7.4.2
|
|
var (
|
|
StatusRangeNotInUse = StatusCodeRange{0, 999}
|
|
StatusRangeProtocol = StatusCodeRange{1000, 2999}
|
|
StatusRangeApplication = StatusCodeRange{3000, 3999}
|
|
StatusRangePrivate = StatusCodeRange{4000, 4999}
|
|
)
|
|
|
|
// Status codes defined by specification.
|
|
// See https://tools.ietf.org/html/rfc6455#section-7.4.1
|
|
const (
|
|
StatusNormalClosure StatusCode = 1000
|
|
StatusGoingAway StatusCode = 1001
|
|
StatusProtocolError StatusCode = 1002
|
|
StatusUnsupportedData StatusCode = 1003
|
|
StatusNoMeaningYet StatusCode = 1004
|
|
StatusInvalidFramePayloadData StatusCode = 1007
|
|
StatusPolicyViolation StatusCode = 1008
|
|
StatusMessageTooBig StatusCode = 1009
|
|
StatusMandatoryExt StatusCode = 1010
|
|
StatusInternalServerError StatusCode = 1011
|
|
StatusTLSHandshake StatusCode = 1015
|
|
|
|
// StatusAbnormalClosure is a special code designated for use in
|
|
// applications.
|
|
StatusAbnormalClosure StatusCode = 1006
|
|
|
|
// StatusNoStatusRcvd is a special code designated for use in applications.
|
|
StatusNoStatusRcvd StatusCode = 1005
|
|
)
|
|
|
|
// In reports whether the code is defined in given range.
|
|
func (s StatusCode) In(r StatusCodeRange) bool {
|
|
return r.Min <= s && s <= r.Max
|
|
}
|
|
|
|
// Empty reports whether the code is empty.
|
|
// Empty code has no any meaning neither app level codes nor other.
|
|
// This method is useful just to check that code is golang default value 0.
|
|
func (s StatusCode) Empty() bool {
|
|
return s == 0
|
|
}
|
|
|
|
// IsNotUsed reports whether the code is predefined in not used range.
|
|
func (s StatusCode) IsNotUsed() bool {
|
|
return s.In(StatusRangeNotInUse)
|
|
}
|
|
|
|
// IsApplicationSpec reports whether the code should be defined by
|
|
// application, framework or libraries specification.
|
|
func (s StatusCode) IsApplicationSpec() bool {
|
|
return s.In(StatusRangeApplication)
|
|
}
|
|
|
|
// IsPrivateSpec reports whether the code should be defined privately.
|
|
func (s StatusCode) IsPrivateSpec() bool {
|
|
return s.In(StatusRangePrivate)
|
|
}
|
|
|
|
// IsProtocolSpec reports whether the code should be defined by protocol specification.
|
|
func (s StatusCode) IsProtocolSpec() bool {
|
|
return s.In(StatusRangeProtocol)
|
|
}
|
|
|
|
// IsProtocolDefined reports whether the code is already defined by protocol specification.
|
|
func (s StatusCode) IsProtocolDefined() bool {
|
|
switch s {
|
|
case StatusNormalClosure,
|
|
StatusGoingAway,
|
|
StatusProtocolError,
|
|
StatusUnsupportedData,
|
|
StatusInvalidFramePayloadData,
|
|
StatusPolicyViolation,
|
|
StatusMessageTooBig,
|
|
StatusMandatoryExt,
|
|
StatusInternalServerError,
|
|
StatusNoStatusRcvd,
|
|
StatusAbnormalClosure,
|
|
StatusTLSHandshake:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// IsProtocolReserved reports whether the code is defined by protocol specification
|
|
// to be reserved only for application usage purpose.
|
|
func (s StatusCode) IsProtocolReserved() bool {
|
|
switch s {
|
|
// [RFC6455]: {1005,1006,1015} is a reserved value and MUST NOT be set as a status code in a
|
|
// Close control frame by an endpoint.
|
|
case StatusNoStatusRcvd, StatusAbnormalClosure, StatusTLSHandshake:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Compiled control frames for common use cases.
|
|
// For construct-serialize optimizations.
|
|
var (
|
|
CompiledPing = MustCompileFrame(NewPingFrame(nil))
|
|
CompiledPong = MustCompileFrame(NewPongFrame(nil))
|
|
CompiledClose = MustCompileFrame(NewCloseFrame(nil))
|
|
|
|
CompiledCloseNormalClosure = MustCompileFrame(closeFrameNormalClosure)
|
|
CompiledCloseGoingAway = MustCompileFrame(closeFrameGoingAway)
|
|
CompiledCloseProtocolError = MustCompileFrame(closeFrameProtocolError)
|
|
CompiledCloseUnsupportedData = MustCompileFrame(closeFrameUnsupportedData)
|
|
CompiledCloseNoMeaningYet = MustCompileFrame(closeFrameNoMeaningYet)
|
|
CompiledCloseInvalidFramePayloadData = MustCompileFrame(closeFrameInvalidFramePayloadData)
|
|
CompiledClosePolicyViolation = MustCompileFrame(closeFramePolicyViolation)
|
|
CompiledCloseMessageTooBig = MustCompileFrame(closeFrameMessageTooBig)
|
|
CompiledCloseMandatoryExt = MustCompileFrame(closeFrameMandatoryExt)
|
|
CompiledCloseInternalServerError = MustCompileFrame(closeFrameInternalServerError)
|
|
CompiledCloseTLSHandshake = MustCompileFrame(closeFrameTLSHandshake)
|
|
)
|
|
|
|
// Header represents websocket frame header.
|
|
// See https://tools.ietf.org/html/rfc6455#section-5.2
|
|
type Header struct {
|
|
Fin bool
|
|
Rsv byte
|
|
OpCode OpCode
|
|
Masked bool
|
|
Mask [4]byte
|
|
Length int64
|
|
}
|
|
|
|
// Rsv1 reports whether the header has first rsv bit set.
|
|
func (h Header) Rsv1() bool { return h.Rsv&bit5 != 0 }
|
|
|
|
// Rsv2 reports whether the header has second rsv bit set.
|
|
func (h Header) Rsv2() bool { return h.Rsv&bit6 != 0 }
|
|
|
|
// Rsv3 reports whether the header has third rsv bit set.
|
|
func (h Header) Rsv3() bool { return h.Rsv&bit7 != 0 }
|
|
|
|
// Rsv creates rsv byte representation from bits.
|
|
func Rsv(r1, r2, r3 bool) (rsv byte) {
|
|
if r1 {
|
|
rsv |= bit5
|
|
}
|
|
if r2 {
|
|
rsv |= bit6
|
|
}
|
|
if r3 {
|
|
rsv |= bit7
|
|
}
|
|
return rsv
|
|
}
|
|
|
|
// RsvBits returns rsv bits from bytes representation.
|
|
func RsvBits(rsv byte) (r1, r2, r3 bool) {
|
|
r1 = rsv&bit5 != 0
|
|
r2 = rsv&bit6 != 0
|
|
r3 = rsv&bit7 != 0
|
|
return r1, r2, r3
|
|
}
|
|
|
|
// Frame represents websocket frame.
|
|
// See https://tools.ietf.org/html/rfc6455#section-5.2
|
|
type Frame struct {
|
|
Header Header
|
|
Payload []byte
|
|
}
|
|
|
|
// NewFrame creates frame with given operation code,
|
|
// flag of completeness and payload bytes.
|
|
func NewFrame(op OpCode, fin bool, p []byte) Frame {
|
|
return Frame{
|
|
Header: Header{
|
|
Fin: fin,
|
|
OpCode: op,
|
|
Length: int64(len(p)),
|
|
},
|
|
Payload: p,
|
|
}
|
|
}
|
|
|
|
// NewTextFrame creates text frame with p as payload.
|
|
// Note that p is not copied.
|
|
func NewTextFrame(p []byte) Frame {
|
|
return NewFrame(OpText, true, p)
|
|
}
|
|
|
|
// NewBinaryFrame creates binary frame with p as payload.
|
|
// Note that p is not copied.
|
|
func NewBinaryFrame(p []byte) Frame {
|
|
return NewFrame(OpBinary, true, p)
|
|
}
|
|
|
|
// NewPingFrame creates ping frame with p as payload.
|
|
// Note that p is not copied.
|
|
// Note that p must have length of MaxControlFramePayloadSize bytes or less due
|
|
// to RFC.
|
|
func NewPingFrame(p []byte) Frame {
|
|
return NewFrame(OpPing, true, p)
|
|
}
|
|
|
|
// NewPongFrame creates pong frame with p as payload.
|
|
// Note that p is not copied.
|
|
// Note that p must have length of MaxControlFramePayloadSize bytes or less due
|
|
// to RFC.
|
|
func NewPongFrame(p []byte) Frame {
|
|
return NewFrame(OpPong, true, p)
|
|
}
|
|
|
|
// NewCloseFrame creates close frame with given close body.
|
|
// Note that p is not copied.
|
|
// Note that p must have length of MaxControlFramePayloadSize bytes or less due
|
|
// to RFC.
|
|
func NewCloseFrame(p []byte) Frame {
|
|
return NewFrame(OpClose, true, p)
|
|
}
|
|
|
|
// NewCloseFrameBody encodes a closure code and a reason into a binary
|
|
// representation.
|
|
//
|
|
// It returns slice which is at most MaxControlFramePayloadSize bytes length.
|
|
// If the reason is too big it will be cropped to fit the limit defined by the
|
|
// spec.
|
|
//
|
|
// See https://tools.ietf.org/html/rfc6455#section-5.5
|
|
func NewCloseFrameBody(code StatusCode, reason string) []byte {
|
|
n := min(2+len(reason), MaxControlFramePayloadSize)
|
|
p := make([]byte, n)
|
|
|
|
crop := min(MaxControlFramePayloadSize-2, len(reason))
|
|
PutCloseFrameBody(p, code, reason[:crop])
|
|
|
|
return p
|
|
}
|
|
|
|
// PutCloseFrameBody encodes code and reason into buf.
|
|
//
|
|
// It will panic if the buffer is too small to accommodate a code or a reason.
|
|
//
|
|
// PutCloseFrameBody does not check buffer to be RFC compliant, but note that
|
|
// by RFC it must be at most MaxControlFramePayloadSize.
|
|
func PutCloseFrameBody(p []byte, code StatusCode, reason string) {
|
|
_ = p[1+len(reason)]
|
|
binary.BigEndian.PutUint16(p, uint16(code))
|
|
copy(p[2:], reason)
|
|
}
|
|
|
|
// MaskFrame masks frame and returns frame with masked payload and Mask header's field set.
|
|
// Note that it copies f payload to prevent collisions.
|
|
// For less allocations you could use MaskFrameInPlace or construct frame manually.
|
|
func MaskFrame(f Frame) Frame {
|
|
return MaskFrameWith(f, NewMask())
|
|
}
|
|
|
|
// MaskFrameWith masks frame with given mask and returns frame
|
|
// with masked payload and Mask header's field set.
|
|
// Note that it copies f payload to prevent collisions.
|
|
// For less allocations you could use MaskFrameInPlaceWith or construct frame manually.
|
|
func MaskFrameWith(f Frame, mask [4]byte) Frame {
|
|
// TODO(gobwas): check CopyCipher ws copy() Cipher().
|
|
p := make([]byte, len(f.Payload))
|
|
copy(p, f.Payload)
|
|
f.Payload = p
|
|
return MaskFrameInPlaceWith(f, mask)
|
|
}
|
|
|
|
// MaskFrameInPlace masks frame and returns frame with masked payload and Mask
|
|
// header's field set.
|
|
// Note that it applies xor cipher to f.Payload without copying, that is, it
|
|
// modifies f.Payload inplace.
|
|
func MaskFrameInPlace(f Frame) Frame {
|
|
return MaskFrameInPlaceWith(f, NewMask())
|
|
}
|
|
|
|
var zeroMask [4]byte
|
|
|
|
// UnmaskFrame unmasks frame and returns frame with unmasked payload and Mask
|
|
// header's field cleared.
|
|
// Note that it copies f payload.
|
|
func UnmaskFrame(f Frame) Frame {
|
|
p := make([]byte, len(f.Payload))
|
|
copy(p, f.Payload)
|
|
f.Payload = p
|
|
return UnmaskFrameInPlace(f)
|
|
}
|
|
|
|
// UnmaskFrameInPlace unmasks frame and returns frame with unmasked payload and
|
|
// Mask header's field cleared.
|
|
// Note that it applies xor cipher to f.Payload without copying, that is, it
|
|
// modifies f.Payload inplace.
|
|
func UnmaskFrameInPlace(f Frame) Frame {
|
|
Cipher(f.Payload, f.Header.Mask, 0)
|
|
f.Header.Masked = false
|
|
f.Header.Mask = zeroMask
|
|
return f
|
|
}
|
|
|
|
// MaskFrameInPlaceWith masks frame with given mask and returns frame
|
|
// with masked payload and Mask header's field set.
|
|
// Note that it applies xor cipher to f.Payload without copying, that is, it
|
|
// modifies f.Payload inplace.
|
|
func MaskFrameInPlaceWith(f Frame, m [4]byte) Frame {
|
|
f.Header.Masked = true
|
|
f.Header.Mask = m
|
|
Cipher(f.Payload, m, 0)
|
|
return f
|
|
}
|
|
|
|
// NewMask creates new random mask.
|
|
func NewMask() (ret [4]byte) {
|
|
binary.BigEndian.PutUint32(ret[:], rand.Uint32())
|
|
return ret
|
|
}
|
|
|
|
// CompileFrame returns byte representation of given frame.
|
|
// In terms of memory consumption it is useful to precompile static frames
|
|
// which are often used.
|
|
func CompileFrame(f Frame) (bts []byte, err error) {
|
|
buf := bytes.NewBuffer(make([]byte, 0, 16))
|
|
err = WriteFrame(buf, f)
|
|
bts = buf.Bytes()
|
|
return bts, err
|
|
}
|
|
|
|
// MustCompileFrame is like CompileFrame but panics if frame can not be
|
|
// encoded.
|
|
func MustCompileFrame(f Frame) []byte {
|
|
bts, err := CompileFrame(f)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return bts
|
|
}
|
|
|
|
func makeCloseFrame(code StatusCode) Frame {
|
|
return NewCloseFrame(NewCloseFrameBody(code, ""))
|
|
}
|
|
|
|
var (
|
|
closeFrameNormalClosure = makeCloseFrame(StatusNormalClosure)
|
|
closeFrameGoingAway = makeCloseFrame(StatusGoingAway)
|
|
closeFrameProtocolError = makeCloseFrame(StatusProtocolError)
|
|
closeFrameUnsupportedData = makeCloseFrame(StatusUnsupportedData)
|
|
closeFrameNoMeaningYet = makeCloseFrame(StatusNoMeaningYet)
|
|
closeFrameInvalidFramePayloadData = makeCloseFrame(StatusInvalidFramePayloadData)
|
|
closeFramePolicyViolation = makeCloseFrame(StatusPolicyViolation)
|
|
closeFrameMessageTooBig = makeCloseFrame(StatusMessageTooBig)
|
|
closeFrameMandatoryExt = makeCloseFrame(StatusMandatoryExt)
|
|
closeFrameInternalServerError = makeCloseFrame(StatusInternalServerError)
|
|
closeFrameTLSHandshake = makeCloseFrame(StatusTLSHandshake)
|
|
)
|