Ground-Zerro / Phobos Public
Code Issues Pull requests Actions Releases View on GitHub ↗
4.8 KB go
/* SPDX-License-Identifier: MIT
 *
 * Phobos
 */

package phobos

import "encoding/binary"

const (
	s5FramePayloadMax = 1024
	s5AccMax          = 2048
	s5EncodeReadMax   = 12288
	s5BufferSize      = 16384

	rtpTCPHeaderSize = 2 + rtpHeaderSize
	tlsRecordHeader  = 5
)

type s5Encoder struct {
	stream rtpStream
	rng    rng32
}

func (e *s5Encoder) frameSize(masking Masking, payload int) int {
	switch masking {
	case MaskingSTUN:
		return stunDataIndHeaderSize + payload + stunPadding(payload)
	case MaskingTLS:
		return tlsRecordHeader + payload
	default:
		return rtpTCPHeaderSize + payload
	}
}

func stunPadding(payload int) int {
	return (4 - payload&3) & 3
}

func (e *s5Encoder) encode(masking Masking, params MediaParams, src, out []byte) int {
	if masking == MaskingMEDIA && !e.stream.initialized {
		e.stream.init(params, &e.rng)
	}
	written := 0
	for len(src) > 0 {
		chunk := min(len(src), s5FramePayloadMax)
		if written+e.frameSize(masking, chunk) > len(out) {
			return -1
		}
		switch masking {
		case MaskingSTUN:
			written += e.encodeSTUN(src[:chunk], out[written:])
		case MaskingTLS:
			written += encodeTLS(src[:chunk], out[written:])
		default:
			written += e.encodeMedia(src[:chunk], out[written:])
		}
		src = src[chunk:]
	}
	return written
}

func (e *s5Encoder) encodeSTUN(src, out []byte) int {
	payload := len(src)
	padding := stunPadding(payload)
	messageLength := 4 + payload + padding

	binary.BigEndian.PutUint16(out, stunDataIndication)
	binary.BigEndian.PutUint16(out[2:], uint16(messageLength))
	copy(out[4:8], stunCookie[:])
	e.rng.fill(out[8:20])
	binary.BigEndian.PutUint16(out[20:], stunAttrData)
	binary.BigEndian.PutUint16(out[22:], uint16(payload))
	copy(out[24:], src)
	clear(out[24+payload : 24+payload+padding])
	return stunDataIndHeaderSize + payload + padding
}

func encodeTLS(src, out []byte) int {
	out[0] = 0x17
	out[1] = 0x03
	out[2] = 0x03
	binary.BigEndian.PutUint16(out[3:], uint16(len(src)))
	copy(out[tlsRecordHeader:], src)
	return tlsRecordHeader + len(src)
}

func (e *s5Encoder) encodeMedia(src, out []byte) int {
	binary.BigEndian.PutUint16(out, uint16(rtpHeaderSize+len(src)))
	e.stream.writeHeader(out[2:])
	copy(out[rtpTCPHeaderSize:], src)
	return rtpTCPHeaderSize + len(src)
}

type s5Decoder struct {
	acc  [s5AccMax]byte
	used int
	work [s5AccMax + s5EncodeReadMax + 16]byte
}

func (d *s5Decoder) decode(masking Masking, params MediaParams, in, out []byte) int {
	if len(in) > s5EncodeReadMax {
		return -1
	}
	total := copy(d.work[:], d.acc[:d.used])
	total += copy(d.work[total:], in)

	pos, written := 0, 0
	for {
		consumed, produced := d.decodeFrame(masking, params, d.work[pos:total], out[written:])
		if consumed < 0 {
			return -1
		}
		if consumed == 0 {
			break
		}
		pos += consumed
		written += produced
	}

	left := total - pos
	if left > s5AccMax {
		return -1
	}
	copy(d.acc[:left], d.work[pos:total])
	d.used = left
	return written
}

func (d *s5Decoder) decodeFrame(masking Masking, params MediaParams, in, out []byte) (int, int) {
	switch masking {
	case MaskingSTUN:
		return decodeSTUNFrame(in, out)
	case MaskingTLS:
		return decodeTLSFrame(in, out)
	default:
		return decodeMediaFrame(params, in, out)
	}
}

func decodeSTUNFrame(in, out []byte) (int, int) {
	if len(in) < stunDataIndHeaderSize {
		return 0, 0
	}
	if [4]byte(in[4:8]) != stunCookie {
		return -1, 0
	}
	if binary.BigEndian.Uint16(in) != stunDataIndication {
		return -1, 0
	}
	total := stunHeaderSize + int(binary.BigEndian.Uint16(in[2:]))
	if total < stunDataIndHeaderSize || total > s5AccMax {
		return -1, 0
	}
	if len(in) < total {
		return 0, 0
	}
	if binary.BigEndian.Uint16(in[20:]) != stunAttrData {
		return -1, 0
	}
	payload := int(binary.BigEndian.Uint16(in[22:]))
	if stunDataIndHeaderSize+payload > total || payload > len(out) {
		return -1, 0
	}
	copy(out, in[stunDataIndHeaderSize:stunDataIndHeaderSize+payload])
	return total, payload
}

func decodeTLSFrame(in, out []byte) (int, int) {
	if len(in) < tlsRecordHeader {
		return 0, 0
	}
	if in[0] != 0x17 || in[1] != 0x03 || in[2] != 0x03 {
		return -1, 0
	}
	payload := int(binary.BigEndian.Uint16(in[3:]))
	if payload < 1 || payload > s5AccMax {
		return -1, 0
	}
	if len(in) < tlsRecordHeader+payload {
		return 0, 0
	}
	if payload > len(out) {
		return -1, 0
	}
	copy(out, in[tlsRecordHeader:tlsRecordHeader+payload])
	return tlsRecordHeader + payload, payload
}

func decodeMediaFrame(params MediaParams, in, out []byte) (int, int) {
	if len(in) < 2 {
		return 0, 0
	}
	length := int(binary.BigEndian.Uint16(in))
	if length < rtpHeaderSize || length > s5AccMax {
		return -1, 0
	}
	if len(in) < 2+length {
		return 0, 0
	}
	if in[2]&0xC0 != 0x80 {
		return -1, 0
	}
	if params.PayloadType != 0 && in[3]&0x7F != params.PayloadType {
		return -1, 0
	}
	payload := length - rtpHeaderSize
	if payload > len(out) {
		return -1, 0
	}
	copy(out, in[rtpTCPHeaderSize:2+length])
	return 2 + length, payload
}