452 lines
9.7 KiB
Go
452 lines
9.7 KiB
Go
package miss
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
"golang.org/x/crypto/chacha20"
|
|
"golang.org/x/crypto/nacl/box"
|
|
)
|
|
|
|
func Dial(rawURL string) (*Client, error) {
|
|
u, err := url.Parse(rawURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
query := u.Query()
|
|
if query.Get("vendor") != "cs2" {
|
|
return nil, fmt.Errorf("miss: unsupported vendor")
|
|
}
|
|
|
|
clientPrivate := query.Get("client_private")
|
|
devicePublic := query.Get("device_public")
|
|
|
|
key, err := calcSharedKey(devicePublic, clientPrivate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
conn, err := net.ListenUDP("udp", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
client := &Client{
|
|
conn: conn,
|
|
addr: &net.UDPAddr{IP: net.ParseIP(u.Host), Port: 32108},
|
|
buf: make([]byte, 1500),
|
|
key: key,
|
|
}
|
|
|
|
clientPublic := query.Get("client_public")
|
|
sign := query.Get("sign")
|
|
|
|
if err = client.login(clientPublic, sign); err != nil {
|
|
_ = conn.Close()
|
|
return nil, err
|
|
}
|
|
|
|
client.chSeq0 = 1
|
|
client.chRaw2 = make(chan []byte, 100)
|
|
go client.worker()
|
|
|
|
return client, nil
|
|
}
|
|
|
|
const (
|
|
CodecH264 = 4
|
|
CodecH265 = 5
|
|
CodecPCM = 1024
|
|
CodecPCMU = 1026
|
|
CodecPCMA = 1027
|
|
CodecOPUS = 1032
|
|
)
|
|
|
|
type Client struct {
|
|
conn *net.UDPConn
|
|
addr *net.UDPAddr
|
|
buf []byte
|
|
key []byte // shared key
|
|
|
|
chSeq0 uint16
|
|
chSeq3 uint16
|
|
chRaw2 chan []byte
|
|
}
|
|
|
|
func (c *Client) RemoteAddr() *net.UDPAddr {
|
|
return c.addr
|
|
}
|
|
|
|
func (c *Client) SetDeadline(t time.Time) error {
|
|
return c.conn.SetDeadline(t)
|
|
}
|
|
|
|
func (c *Client) Close() error {
|
|
return c.conn.Close()
|
|
}
|
|
|
|
const (
|
|
magic = 0xF1
|
|
magicDrw = 0xD1
|
|
msgLanSearch = 0x30
|
|
msgPunchPkt = 0x41
|
|
msgP2PRdy = 0x42
|
|
msgDrw = 0xD0
|
|
msgDrwAck = 0xD1
|
|
msgAlive = 0xE0
|
|
|
|
cmdAuthReq = 0x100
|
|
cmdAuthRes = 0x101
|
|
cmdVideoStart = 0x102
|
|
cmdVideoStop = 0x103
|
|
cmdAudioStart = 0x104
|
|
cmdAudioStop = 0x105
|
|
cmdSpeakerStartReq = 0x106
|
|
cmdSpeakerStartRes = 0x107
|
|
cmdSpeakerStop = 0x108
|
|
cmdStreamCtrlReq = 0x109
|
|
cmdStreamCtrlRes = 0x10A
|
|
cmdGetAudioFormatReq = 0x10B
|
|
cmdGetAudioFormatRes = 0x10C
|
|
cmdPlaybackReq = 0x10D
|
|
cmdPlaybackRes = 0x10E
|
|
cmdDevInfoReq = 0x110
|
|
cmdDevInfoRes = 0x111
|
|
cmdMotorReq = 0x112
|
|
cmdMotorRes = 0x113
|
|
cmdEncoded = 0x1001
|
|
)
|
|
|
|
func (c *Client) login(clientPublic, sign string) error {
|
|
_ = c.conn.SetDeadline(time.Now().Add(core.ConnDialTimeout))
|
|
|
|
buf, err := c.writeAndWait([]byte{magic, msgLanSearch, 0, 0}, msgPunchPkt)
|
|
if err != nil {
|
|
return fmt.Errorf("miss: read punch: %w", err)
|
|
}
|
|
|
|
_, err = c.writeAndWait(buf, msgP2PRdy)
|
|
if err != nil {
|
|
return fmt.Errorf("miss: read ready: %w", err)
|
|
}
|
|
|
|
_, _ = c.conn.WriteToUDP([]byte{magic, msgAlive, 0, 0}, c.addr)
|
|
|
|
s := fmt.Sprintf(`{"public_key":"%s","sign":"%s","uuid":"","support_encrypt":0}`, clientPublic, sign)
|
|
buf, err = c.writeAndWait(marshalCmd(0, 0, cmdAuthReq, []byte(s)), msgDrw)
|
|
if err != nil {
|
|
return fmt.Errorf("miss: read auth: %w", err)
|
|
}
|
|
|
|
if !strings.Contains(string(buf[16:]), `"result":"success"`) {
|
|
return fmt.Errorf("miss: read auth: %s", buf[16:])
|
|
}
|
|
|
|
_, _ = c.conn.WriteToUDP([]byte{magic, msgDrwAck, 0, 6, magicDrw, 0, 0, 1, 0, 0}, c.addr)
|
|
|
|
_ = c.conn.SetDeadline(time.Time{})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) writeAndWait(b []byte, waitMsg uint8) ([]byte, error) {
|
|
if _, err := c.conn.WriteToUDP(b, c.addr); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for {
|
|
n, addr, err := c.conn.ReadFromUDP(c.buf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if string(addr.IP) != string(c.addr.IP) {
|
|
continue // skip messages from another IP
|
|
}
|
|
|
|
if n >= 16 && c.buf[0] == magic && c.buf[1] == waitMsg {
|
|
if waitMsg == msgPunchPkt {
|
|
c.addr.Port = addr.Port
|
|
}
|
|
return c.buf[:n], nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Client) VideoStart(channel, quality, audio uint8) error {
|
|
buf := binary.BigEndian.AppendUint32(nil, cmdVideoStart)
|
|
if channel == 0 {
|
|
buf = fmt.Appendf(buf, `{"videoquality":%d,"enableaudio":%d}`, quality, audio)
|
|
} else {
|
|
buf = fmt.Appendf(buf, `{"videoquality":-1,"videoquality2":%d,"enableaudio":%d}`, quality, audio)
|
|
}
|
|
buf, err := encode(c.key, buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
buf = marshalCmd(0, c.chSeq0, cmdEncoded, buf)
|
|
c.chSeq0++
|
|
|
|
_, err = c.conn.WriteToUDP(buf, c.addr)
|
|
return err
|
|
}
|
|
|
|
func (c *Client) SpeakerStart() error {
|
|
buf := binary.BigEndian.AppendUint32(nil, cmdSpeakerStartReq)
|
|
buf, err := encode(c.key, buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
buf = marshalCmd(0, c.chSeq0, cmdEncoded, buf)
|
|
c.chSeq0++
|
|
|
|
_, err = c.conn.WriteToUDP(buf, c.addr)
|
|
return err
|
|
}
|
|
|
|
func (c *Client) ReadPacket() (*Packet, error) {
|
|
b, ok := <-c.chRaw2
|
|
if !ok {
|
|
return nil, fmt.Errorf("miss: read raw: i/o timeout")
|
|
}
|
|
return unmarshalPacket(c.key, b)
|
|
}
|
|
|
|
func unmarshalPacket(key, b []byte) (*Packet, error) {
|
|
n := uint32(len(b))
|
|
|
|
if n < 32 {
|
|
return nil, fmt.Errorf("miss: packet header too small")
|
|
}
|
|
|
|
if l := binary.LittleEndian.Uint32(b); l+32 != n {
|
|
return nil, fmt.Errorf("miss: packet payload has wrong length")
|
|
}
|
|
|
|
payload, err := decode(key, b[32:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Packet{
|
|
CodecID: binary.LittleEndian.Uint32(b[4:]),
|
|
Sequence: binary.LittleEndian.Uint32(b[8:]),
|
|
Flags: binary.LittleEndian.Uint32(b[12:]),
|
|
Timestamp: binary.LittleEndian.Uint64(b[16:]),
|
|
Payload: payload,
|
|
}, nil
|
|
}
|
|
|
|
func (c *Client) WriteAudio(codecID uint32, payload []byte) error {
|
|
payload, err := encode(c.key, payload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
n := uint32(len(payload))
|
|
|
|
const hdrOffset = 12
|
|
const hdrSize = 32
|
|
|
|
buf := make([]byte, n+hdrOffset+hdrSize)
|
|
buf[0] = magic
|
|
buf[1] = msgDrw
|
|
binary.BigEndian.PutUint16(buf[2:], uint16(n+8+hdrSize))
|
|
|
|
buf[4] = magicDrw
|
|
buf[5] = 3 // channel
|
|
binary.BigEndian.PutUint16(buf[6:], c.chSeq3)
|
|
|
|
binary.BigEndian.PutUint32(buf[8:], n+hdrSize)
|
|
|
|
binary.LittleEndian.PutUint32(buf[hdrOffset:], n)
|
|
binary.LittleEndian.PutUint32(buf[hdrOffset+4:], codecID)
|
|
binary.LittleEndian.PutUint64(buf[hdrOffset+16:], uint64(time.Now().UnixMilli()))
|
|
copy(buf[hdrOffset+hdrSize:], payload)
|
|
|
|
c.chSeq3++
|
|
|
|
_, err = c.conn.WriteToUDP(buf, c.addr)
|
|
return err
|
|
}
|
|
|
|
func (c *Client) worker() {
|
|
defer close(c.chRaw2)
|
|
|
|
chAck := []uint16{1, 0, 0, 0}
|
|
|
|
var ch2WaitSize int
|
|
var ch2WaitData []byte
|
|
|
|
for {
|
|
n, addr, err := c.conn.ReadFromUDP(c.buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
//log.Printf("<- %.20x...", c.buf[:n])
|
|
|
|
if string(addr.IP) != string(c.addr.IP) || n < 8 || c.buf[0] != magic {
|
|
//log.Printf("unknown msg: %x", c.buf[:n])
|
|
continue // skip messages from another IP
|
|
}
|
|
|
|
switch c.buf[1] {
|
|
case msgDrw:
|
|
ch := c.buf[5]
|
|
seqHI := c.buf[6]
|
|
seqLO := c.buf[7]
|
|
|
|
if chAck[ch] != uint16(seqHI)<<8|uint16(seqLO) {
|
|
continue
|
|
}
|
|
chAck[ch]++
|
|
|
|
//log.Printf("%.40x", c.buf)
|
|
|
|
ack := []byte{magic, msgDrwAck, 0, 6, magicDrw, ch, 0, 1, seqHI, seqLO}
|
|
if _, err = c.conn.WriteToUDP(ack, c.addr); err != nil {
|
|
return
|
|
}
|
|
|
|
switch ch {
|
|
case 0:
|
|
//log.Printf("data ch0 %x", c.buf[:n])
|
|
//size := binary.BigEndian.Uint32(c.buf[8:])
|
|
//if binary.BigEndian.Uint32(c.buf[12:]) == cmdEncoded {
|
|
// raw, _ := decode(c.key, c.buf[16:12+size])
|
|
// log.Printf("cmd enc %x", raw)
|
|
//} else {
|
|
// log.Printf("cmd raw %x", c.buf[12:12+size])
|
|
//}
|
|
|
|
case 2:
|
|
ch2WaitData = append(ch2WaitData, c.buf[8:n]...)
|
|
|
|
for len(ch2WaitData) > 4 {
|
|
if ch2WaitSize == 0 {
|
|
ch2WaitSize = int(binary.BigEndian.Uint32(ch2WaitData))
|
|
ch2WaitData = ch2WaitData[4:]
|
|
}
|
|
if ch2WaitSize <= len(ch2WaitData) {
|
|
c.chRaw2 <- ch2WaitData[:ch2WaitSize]
|
|
ch2WaitData = ch2WaitData[ch2WaitSize:]
|
|
ch2WaitSize = 0
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
default:
|
|
log.Printf("!!! unknown chanel: %x", c.buf[:n])
|
|
}
|
|
|
|
case msgDrwAck: // skip it
|
|
|
|
default:
|
|
log.Printf("!!! unknown msg type: %x", c.buf[:n])
|
|
}
|
|
}
|
|
}
|
|
|
|
func marshalCmd(channel byte, seq uint16, cmd uint32, payload []byte) []byte {
|
|
size := len(payload)
|
|
buf := make([]byte, 4+4+4+4+size)
|
|
|
|
// 1. message header (4 bytes)
|
|
buf[0] = magic
|
|
buf[1] = msgDrw
|
|
binary.BigEndian.PutUint16(buf[2:], uint16(4+4+4+size))
|
|
|
|
// 2. drw? header (4 bytes)
|
|
buf[4] = magicDrw
|
|
buf[5] = channel
|
|
binary.BigEndian.PutUint16(buf[6:], seq)
|
|
|
|
// 3. payload size (4 bytes)
|
|
binary.BigEndian.PutUint32(buf[8:], uint32(4+size))
|
|
|
|
// 4. payload command (4 bytes)
|
|
binary.BigEndian.PutUint32(buf[12:], cmd)
|
|
|
|
// 5. payload
|
|
copy(buf[16:], payload)
|
|
|
|
return buf
|
|
}
|
|
|
|
func calcSharedKey(devicePublic, clientPrivate string) ([]byte, error) {
|
|
var sharedKey, publicKey, privateKey [32]byte
|
|
if _, err := hex.Decode(publicKey[:], []byte(devicePublic)); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := hex.Decode(privateKey[:], []byte(clientPrivate)); err != nil {
|
|
return nil, err
|
|
}
|
|
box.Precompute(&sharedKey, &publicKey, &privateKey)
|
|
return sharedKey[:], nil
|
|
}
|
|
|
|
func encode(key, src []byte) ([]byte, error) {
|
|
dst := make([]byte, len(src)+8)
|
|
|
|
if _, err := rand.Read(dst[:8]); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
nonce := make([]byte, 12)
|
|
copy(nonce[4:], dst[:8])
|
|
|
|
c, err := chacha20.NewUnauthenticatedCipher(key, nonce)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
c.XORKeyStream(dst[8:], src)
|
|
|
|
return dst, nil
|
|
}
|
|
|
|
func decode(key, src []byte) ([]byte, error) {
|
|
nonce := make([]byte, 12)
|
|
copy(nonce[4:], src[:8])
|
|
|
|
c, err := chacha20.NewUnauthenticatedCipher(key, nonce)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dst := make([]byte, len(src)-8)
|
|
c.XORKeyStream(dst, src[8:])
|
|
|
|
return dst, nil
|
|
}
|
|
|
|
type Packet struct {
|
|
//Length uint32
|
|
CodecID uint32
|
|
Sequence uint32
|
|
Flags uint32
|
|
Timestamp uint64 // msec
|
|
//TimestampS uint32
|
|
//Reserved uint32
|
|
Payload []byte
|
|
}
|
|
|
|
func GenerateKey() ([]byte, []byte, error) {
|
|
public, private, err := box.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return public[:], private[:], err
|
|
}
|