299 lines
6.5 KiB
Go
299 lines
6.5 KiB
Go
package miss
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"time"
|
|
|
|
"github.com/AlexxIT/go2rtc/pkg/xiaomi/cs2"
|
|
"github.com/AlexxIT/go2rtc/pkg/xiaomi/tutk"
|
|
"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()
|
|
|
|
c := &Client{}
|
|
|
|
c.key, err = calcSharedKey(query.Get("device_public"), query.Get("client_private"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch s := query.Get("vendor"); s {
|
|
case "cs2":
|
|
c.conn, err = cs2.Dial(u.Host, query.Get("transport"))
|
|
case "tutk":
|
|
c.conn, err = tutk.Dial(u.Host, query.Get("uid"))
|
|
default:
|
|
return nil, fmt.Errorf("miss: unsupported vendor %s", s)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = c.login(query.Get("client_public"), query.Get("sign"))
|
|
if err != nil {
|
|
_ = c.conn.Close()
|
|
return nil, err
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
const (
|
|
CodecH264 = 4
|
|
CodecH265 = 5
|
|
CodecPCM = 1024
|
|
CodecPCMU = 1026
|
|
CodecPCMA = 1027
|
|
CodecOPUS = 1032
|
|
)
|
|
|
|
type Conn interface {
|
|
Protocol() string
|
|
ReadCommand() (cmd uint16, data []byte, err error)
|
|
WriteCommand(cmd uint16, data []byte) error
|
|
ReadPacket() ([]byte, error)
|
|
WritePacket(data []byte) error
|
|
RemoteAddr() net.Addr
|
|
SetDeadline(t time.Time) error
|
|
Close() error
|
|
}
|
|
|
|
type Client struct {
|
|
conn Conn
|
|
key []byte
|
|
}
|
|
|
|
func (c *Client) Protocol() string {
|
|
return c.conn.Protocol()
|
|
}
|
|
|
|
func (c *Client) RemoteAddr() net.Addr {
|
|
return c.conn.RemoteAddr()
|
|
}
|
|
|
|
func (c *Client) SetDeadline(t time.Time) error {
|
|
return c.conn.SetDeadline(t)
|
|
}
|
|
|
|
func (c *Client) Close() error {
|
|
return c.conn.Close()
|
|
}
|
|
|
|
const (
|
|
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 {
|
|
s := fmt.Sprintf(`{"public_key":"%s","sign":"%s","uuid":"","support_encrypt":0}`, clientPublic, sign)
|
|
if err := c.conn.WriteCommand(cmdAuthReq, []byte(s)); err != nil {
|
|
return err
|
|
}
|
|
|
|
_, data, err := c.conn.ReadCommand()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !bytes.Contains(data, []byte(`"result":"success"`)) {
|
|
return fmt.Errorf("miss: auth: %s", data)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) WriteCommand(data []byte) error {
|
|
data, err := encode(c.key, data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.conn.WriteCommand(cmdEncoded, data)
|
|
}
|
|
|
|
func (c *Client) VideoStart(channel, quality, audio uint8) error {
|
|
data := binary.BigEndian.AppendUint32(nil, cmdVideoStart)
|
|
if channel == 0 {
|
|
data = fmt.Appendf(data, `{"videoquality":%d,"enableaudio":%d}`, quality, audio)
|
|
} else {
|
|
data = fmt.Appendf(data, `{"videoquality":-1,"videoquality2":%d,"enableaudio":%d}`, quality, audio)
|
|
}
|
|
return c.WriteCommand(data)
|
|
}
|
|
|
|
func (c *Client) AudioStart() error {
|
|
data := binary.BigEndian.AppendUint32(nil, cmdAudioStart)
|
|
return c.WriteCommand(data)
|
|
}
|
|
|
|
func (c *Client) SpeakerStart() error {
|
|
data := binary.BigEndian.AppendUint32(nil, cmdSpeakerStartReq)
|
|
return c.WriteCommand(data)
|
|
}
|
|
|
|
func (c *Client) ReadPacket() (*Packet, error) {
|
|
data, err := c.conn.ReadPacket()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("miss: read media: %w", err)
|
|
}
|
|
return unmarshalPacket(c.key, data)
|
|
}
|
|
|
|
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) // new payload will have new size!
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
const hdrSize = 32
|
|
n := uint32(len(payload))
|
|
|
|
data := make([]byte, hdrSize+n)
|
|
binary.LittleEndian.PutUint32(data, n)
|
|
binary.LittleEndian.PutUint32(data[4:], codecID)
|
|
binary.LittleEndian.PutUint64(data[16:], uint64(time.Now().UnixMilli())) // not really necessary
|
|
copy(data[hdrSize:], payload)
|
|
return c.conn.WritePacket(data)
|
|
}
|
|
|
|
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 (p *Packet) SampleRate() uint32 {
|
|
// flag: 1 0011 000 - sample rate 16000
|
|
// flag: 100 00 01 0000 000 - sample rate 8000
|
|
v := (p.Flags >> 3) & 0b1111
|
|
if v != 0 {
|
|
return 16000
|
|
}
|
|
return 8000
|
|
}
|
|
|
|
//func (p *Packet) AudioUnknown1() byte {
|
|
// return byte((p.Flags >> 7) & 0b11)
|
|
//}
|
|
//
|
|
//func (p *Packet) AudioUnknown2() byte {
|
|
// return byte((p.Flags >> 9) & 0b11)
|
|
//}
|
|
|
|
func GenerateKey() ([]byte, []byte, error) {
|
|
public, private, err := box.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return public[:], private[:], err
|
|
}
|