Files
konradandCursor 2c621d4b7f Let the config choose protocol 2 or 3, and fall back for one session only.
The default stays v2. A reflector that refuses that version and names the other gets one session of it, then the station announces the configured protocol again.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-22 16:28:42 +02:00

258 lines
5.6 KiB
Go

package station
import (
"encoding/binary"
"fmt"
"io"
"regexp"
"strconv"
)
const (
tcpHeartbeat = 1
tcpProtoVer = 5
tcpProtoDown = 6
tcpAuthChallenge = 10
tcpAuthResponse = 11
tcpAuthOK = 12
tcpError = 13
tcpStartEncReq = 14
tcpStartEnc = 15
tcpCsrReq = 16
tcpCsr = 17
tcpClientCert = 18
tcpCAInfo = 19
tcpCABundleReq = 20
tcpCABundle = 21
tcpServerInfo = 100
tcpNodeList = 101
tcpNodeJoined = 102
tcpNodeLeft = 103
tcpTalkerStart = 104
tcpTalkerStop = 105
tcpSelectTG = 106
tcpRequestQSY = 109
tcpStateEvent = 110
tcpNodeInfo = 111
tcpSignal = 112
tcpTxStatus = 113
tcpStartUDPEnc = 114
udpHeartbeat = 1
udpAudio = 101
udpFlush = 102
udpAllFlushed = 103
maxFrame = 32768
maxPreauth20 = 64
)
type frame struct {
typ uint16
body []byte
}
func writeFrame(w io.Writer, typ uint16, body []byte) error {
payload := make([]byte, 2+len(body))
binary.BigEndian.PutUint16(payload[:2], typ)
copy(payload[2:], body)
var hdr [4]byte
binary.BigEndian.PutUint32(hdr[:], uint32(len(payload)))
if _, err := w.Write(hdr[:]); err != nil {
return err
}
_, err := w.Write(payload)
return err
}
func readFrame(r io.Reader) (frame, error) {
return readFrameMax(r, maxFrame)
}
func readFrameMax(r io.Reader, max uint32) (frame, error) {
var hdr [4]byte
if _, err := io.ReadFull(r, hdr[:]); err != nil {
return frame{}, err
}
n := binary.BigEndian.Uint32(hdr[:])
if n < 2 || n > max {
return frame{}, fmt.Errorf("frame length %d", n)
}
buf := make([]byte, n)
if _, err := io.ReadFull(r, buf); err != nil {
return frame{}, err
}
return frame{typ: binary.BigEndian.Uint16(buf[:2]), body: buf[2:]}, nil
}
func appendU16(b []byte, v uint16) []byte {
var buf [2]byte
binary.BigEndian.PutUint16(buf[:], v)
return append(b, buf[:]...)
}
func appendString(b []byte, s string) []byte {
b = appendU16(b, uint16(len(s)))
return append(b, s...)
}
func appendBlob(b []byte, p []byte) []byte {
b = appendU16(b, uint16(len(p)))
return append(b, p...)
}
func parseU16(b []byte) (uint16, []byte, error) {
if len(b) < 2 {
return 0, nil, io.ErrUnexpectedEOF
}
return binary.BigEndian.Uint16(b[:2]), b[2:], nil
}
func parseU32(b []byte) (uint32, []byte, error) {
if len(b) < 4 {
return 0, nil, io.ErrUnexpectedEOF
}
return binary.BigEndian.Uint32(b[:4]), b[4:], nil
}
func parseString(b []byte) (string, []byte, error) {
n, rest, err := parseU16(b)
if err != nil {
return "", nil, err
}
if len(rest) < int(n) {
return "", nil, io.ErrUnexpectedEOF
}
return string(rest[:n]), rest[n:], nil
}
func parseBlob(b []byte) ([]byte, []byte, error) {
n, rest, err := parseU16(b)
if err != nil {
return nil, nil, err
}
if len(rest) < int(n) {
return nil, nil, io.ErrUnexpectedEOF
}
return rest[:n], rest[n:], nil
}
func parseStringList(b []byte) ([]string, []byte, error) {
n, rest, err := parseU16(b)
if err != nil {
return nil, nil, err
}
out := make([]string, 0, n)
for range int(n) {
s, next, err := parseString(rest)
if err != nil {
return nil, nil, err
}
out = append(out, s)
rest = next
}
return out, rest, nil
}
func parseServerInfo(b []byte) (uint16, []string, error) {
_, rest, err := parseU16(b)
if err != nil {
return 0, nil, err
}
id, rest, err := parseU16(rest)
if err != nil {
return 0, nil, err
}
_, rest, err = parseStringList(rest)
if err != nil {
return 0, nil, err
}
codecs, _, err := parseStringList(rest)
if err != nil {
return 0, nil, err
}
return id, codecs, nil
}
func versionWire(major, minor uint16) []byte {
return appendU16(appendU16(nil, major), minor)
}
func parseVersion(b []byte) (uint16, uint16, error) {
major, rest, err := parseU16(b)
if err != nil {
return 0, 0, err
}
minor, rest, err := parseU16(rest)
if err != nil {
return 0, 0, err
}
if len(rest) != 0 {
return 0, 0, io.ErrUnexpectedEOF
}
return major, minor, nil
}
var atMostVersion = regexp.MustCompile(`^Unsupported protocol version \d+\.\d+\. May be at most (\d+)\.(\d+)\.$`)
var atLeastVersion = regexp.MustCompile(`^Unsupported protocol version \d+\.\d+\. Must be at least (\d+)\.(\d+)\.$`)
// wantsOther reports that the reflector refused the announced major and named the other one.
func wantsOther(announced uint16, msg string) bool {
if announced >= 3 {
return fallback20(msg)
}
return fallback30(msg)
}
// acceptsOther reports that a downgrade names the protocol this station did not announce.
func acceptsOther(announced, gotMajor, gotMinor uint16) bool {
if announced >= 3 {
return gotMajor == 2
}
return gotMajor == 3 && gotMinor == 0
}
// fallback20 reports that the reflector's maximum is at least 2.0 and below 3.0.
func fallback20(msg string) bool {
m := atMostVersion.FindStringSubmatch(msg)
if m == nil {
return false
}
major, err1 := strconv.Atoi(m[1])
minor, err2 := strconv.Atoi(m[2])
if err1 != nil || err2 != nil {
return false
}
return !versionBelow(major, minor, 2, 0) && versionBelow(major, minor, 3, 0)
}
// fallback30 reports that the reflector's minimum is exactly 3.0.
func fallback30(msg string) bool {
m := atLeastVersion.FindStringSubmatch(msg)
if m == nil {
return false
}
major, err1 := strconv.Atoi(m[1])
minor, err2 := strconv.Atoi(m[2])
if err1 != nil || err2 != nil {
return false
}
return major == 3 && minor == 0
}
func versionBelow(major, minor, maj, min int) bool {
if major != maj {
return major < maj
}
return minor < min
}
func hasOPUS(codecs []string) bool {
for _, c := range codecs {
if c == "OPUS" {
return true
}
}
return false
}